pax_global_header00006660000000000000000000000064124144664730014525gustar00rootroot0000000000000052 comment=c14560c2b18833ff087349d2a986cd076bade93f gitolite3-3.6.1/000077500000000000000000000000001241446647300134375ustar00rootroot00000000000000gitolite3-3.6.1/CHANGELOG000066400000000000000000000155071241446647300146610ustar00rootroot000000000000002014-06-22 v3.6.1 experimental rc format convertor for "<= 3.3" users who have already upgraded the *code* to ">= v3.4". Program is in contrib/utils. giving shell access to a few users got a lot easier (see comments in the rc file). allow logging to syslog as well (see comments in the rc file) new 'motd' command redis caching redone and now in core; see http://gitolite.com/gitolite/cache.html 2014-05-09 v3.6 (cool stuff) the access command can now help you debug your rules / understand how a specific access decision was arrived at. mirroring: since mirroring is asynchronous (by default anyway), when a 'git push --mirror' fails, you may not know it unless you look in the log file on the server. Now gitolite captures the info and -- if the word 'fatal' appears anywhere within it, it saves the entire output and prints it to STDERR for anyone who reads or writes the repo on the *master* server, until the error condition clears up. mirroring: allow 'nosync' slaves -- no attempt to automatically push to these slaves will be made. Instead, you have to manually (or via cron, etc) trigger pushes. (backward compat breakage) the old v2 syntax for specifying gitweb owner and description is no longer supported. macros now allow strings as arguments (thanks to Jason Donenfeld for the idea/problem). the 'info' command can print in JSON format if asked to. repo-specific hooks: now you can specify more than one, and gitolite runs all of them in sequence. new trigger 'expand-deny-messages' to show more details when access is denied. git-annex support is finally in master, yaaay! new 'readme' command, modelled after 'desc'. Apparently gitweb can use a README.html file in the *bare* repo directory -- who knew! 2013-10-14 v3.5.3 catch undefined groupnames (when possible) mirroring: async push to slaves (some portability fixes) (a couple of contrib scripts - querying IPA based LDAP servers for group membership, and user key management) allow groups in subconf files (this *may* slow down compilation in extreme cases) make adding repo-specific hooks easier (see cust.mkd or cust.html online for docs) smart http now supports git 1.8.2 and above (which changed the protocol requirements a wee bit) 2013-07-10 v3.5.2 allow ENV vars to be set from repo options, for use in triggers and hooks bug-fix: the new set-default-roles feature was being invoked on every run of "perms" and overriding it! 2013-03-24 v3.5 (2 minor backward compat breakages) 1. 'DEFAULT_ROLE_PERMS' replaced by per repo 'default.roles' option 2. 'gitolite list-memberships' now requires a '-r' or a '-u' flag new 'gitolite owns' command (thanks to Kevin Pulo) 2013-03-05 v3.4 new rc file format makes it much easier to enable specific features 2012-12-29 v3.3 bug fix: gl-perms propagation to slaves broke sometime after v3.2 (so if you're only picking up tagged releases you're OK) the "D" command now allows rm/unlock to be totally disabled new trigger: update-gitweb-daemon-from-options; another way to update gitweb and daemon access lists new 'create' command for explicit wild repo creation, and new AutoCreate trigger to control auto-creation allow simple macros in conf file 2012-11-14 v3.2 major efficiency boost for large setups optional support for multi-line pubkeys; see src/triggers/post-compile/ssh-authkeys-split bug fix for not creating gl-conf when repo para has only config lines and no access rules new 'bg' trigger command to put long jobs started from a trigger into background %GL_REPO and %GL_CREATOR now work for 'option's also test suite now much more BSD friendly 2012-10-05 v3.1 (security) fix path traversal on wild repos new %GL_CREATOR variable for git-config lines rsync command to create and send bundles automagically migrated 'who-pushed' logical expressions on refexes!!! 2012-06-27 v3.04 documentation graduated and moved out of parents house :) new trigger for 'repo specific umask' new 'list-dangling-repos' command new LOCAL_CODE rc var; allow admin specified programs to override system-installed ones new 'upstream' trigger-cum-command to maintain local copies of external repos new 'sudo' command minor backward compat breakage in 'gitolite query-rc' 'perms' command can now create repo if needed migrated 'symbolic-ref' command 'gitolite setup --hooks-only' 2012-05-23 v3.03 fix major bug that allowed an admin to get a shell 2012-05-20 v3.02 packaging instructions fixed up and smoke tested make it easier to give some users a full shell allow aliasing a repo to another name simulate POST_CREATE for new normal (non-wild) repos (just for kicks) a VREF that allows for voting on changes to a branch bug fix: smart http was not running PRE_ and POST_GIT triggers htpasswd migrated 2012-04-29 v3.01 mostly BSD and Solaris compat also fork command added 2012-04-18 v3.0 first release to "master" This is a compete rewrite of gitolite; please see documentation before upgrading. gitolite3-3.6.1/CONTRIBUTING000066400000000000000000000024431241446647300152740ustar00rootroot00000000000000Go to http://gitolite.com/gitolite/index.html#contact for information on contacting me, the mailing list, and IRC channel. *Unless you are reporting what you think is a security issue, I prefer you send to the mailing list, not to me directly.* Please DO NOT send messages via github's "issues" system, linkedin comments/discussion, stackoverflow questions, google+, and any other Web 3.0 "coolness". (The issues system does have an email interface, but it is not a substitute for email. I can't cc anyone else when I want to, for instance. Well I can, but any response the original requester then makes using the website will not get cc-d to the person I cc-d). Please send patches *via email*, not as github pull requests. Again, if you think it's a security issue, send it directly to my gmail address, but otherwise please send it to the mailing list, so others can see it and comment on it. The preferred format is the files created by git-format-patch, as attachments. However, if your repo has a public clone URL, you can make a new branch just for this fix, and send the repo URL and branch name to the mailing list. (If you do send me a github pull request, I may take it if it's a trivial patch, but otherwise I'll ask you to close the pull request, then read this URL for how to send me the patch.) gitolite3-3.6.1/COPYING000066400000000000000000000355021241446647300144770ustar00rootroot00000000000000 GNU GENERAL PUBLIC LICENSE Version 2, June 1991 Copyright (C) 1989, 1991 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This General Public License applies to most of the Free Software Foundation's software and to any other program whose authors commit to using it. (Some other Free Software Foundation software is covered by the GNU Lesser General Public License instead.) You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things. To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it. For example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software. Also, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations. Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the program proprietary. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all. The precise terms and conditions for copying, distribution and modification follow. GNU GENERAL PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The "Program", below, refers to any such program or work, and a "work based on the Program" means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term "modification".) Each licensee is addressed as "you". Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does. 1. You may copy and distribute verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Program or any portion of it, thus forming a work based on the Program, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a) You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change. b) You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License. c) If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Program. In addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following: a) Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, b) Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, c) Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.) The source code for a work means the preferred form of the work for making modifications to it. For an executable work, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the executable. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. If distribution of executable or object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code. 4. You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 5. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it. 6. Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License. 7. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Program. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 8. If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 9. The Free Software Foundation may publish revised and/or new versions of the General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation. 10. If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. gitolite3-3.6.1/README.txt000066400000000000000000000325411241446647300151420ustar00rootroot00000000000000Github-users: click the 'wiki' link before sending me anything via github. Existing users: this is gitolite v3.x. If you are upgrading from v2.x this file will not suffice; you *must* check the online docs (see below for URL). ------------------------------------------------------------------------ This file contains BASIC DOCUMENTATION ONLY. * It is suitable for a fresh, ssh-based, installation of gitolite and basic usage of its most important features. * It is NOT meant to be exhaustive or detailed. The COMPLETE DOCUMENTATION is at: http://gitolite.com/gitolite/master-toc.html Please go there for what/why/how, concepts, background, troubleshooting, more details on what is covered here, or advanced features not covered here. ------------------------------------------------------------------------ BASIC DOCUMENTATION FOR GITOLITE ================================ This file contains the following sections: INSTALLATION AND SETUP ADDING USERS AND REPOS HELP FOR YOUR USERS BASIC SYNTAX ACCESS RULES GROUPS COMMANDS THE 'rc' FILE GIT-CONFIG GIT-DAEMON GITWEB MIGRATING FROM v2 CONTACT AND SUPPORT LICENSE ------------------------------------------------------------------------ INSTALLATION AND SETUP ---------------------- Server requirements: * any unix system * sh * git 1.6.6+ * perl 5.8.8+ * openssh 5.0+ * a dedicated userid to host the repos (in this document, we assume it is 'git'), with shell access ONLY by 'su - git' from some other userid on the same server. Steps to install: * login as 'git' as described above * make sure ~/.ssh/authorized_keys is empty or non-existent * make sure your ssh public key from your workstation is available at $HOME/YourName.pub * run the following commands: git clone git://github.com/sitaramc/gitolite mkdir -p $HOME/bin gitolite/install -to $HOME/bin gitolite setup -pk YourName.pub If the last command doesn't run perhaps 'bin' in not in your 'PATH'. You can either add it, or just run: $HOME/bin/gitolite setup -pk YourName.pub ADDING USERS AND REPOS ---------------------- Do NOT add new repos or users manually on the server. Gitolite users, repos, and access rules are maintained by making changes to a special repo called 'gitolite-admin' and pushing those changes to the server. ---- To administer your gitolite installation, start by doing this on your workstation (if you have not already done so): git clone git@host:gitolite-admin **NOTE**: if you are asked for a password, something has gone wrong. Now if you 'cd gitolite-admin', you will see two subdirectories in it: 'conf' and 'keydir'. To add new users alice, bob, and carol, obtain their public keys and add them to 'keydir' as alice.pub, bob.pub, and carol.pub respectively. To add a new repo 'foo' and give different levels of access to these users, edit the file 'conf/gitolite.conf' and add lines like this: repo foo RW+ = alice RW = bob R = carol See the 'ACCESS RULES' section later for more details. Once you have made these changes, do something like this: git add conf git add keydir git commit -m 'added foo, gave access to alice, bob, carol' git push When the push completes, gitolite will add the new users to ~/.ssh/authorized_keys on the server, as well as create a new, empty, repo called 'foo'. HELP FOR YOUR USERS ------------------- Once a user has sent you their public key and you have added them as specified above and given them access, you have to tell them what URL to access their repos at. This is usually 'git clone git@host:reponame'; see man git-clone for other forms. **NOTE**: again, if they are asked for a password, something is wrong. If they need to know what repos they have access to, they just have to run 'ssh git@host info'; see 'COMMANDS' section later for more on this. BASIC SYNTAX ------------ The basic syntax of the conf file is very simple. * Everything is space separated; there are no commas, semicolons, etc., in the syntax. * Comments are in the usual perl/shell style. * User and repo names are as simple as possible; they must start with an alphanumeric, but after that they can also contain '.', '_', or '-'. Usernames can optionally be followed by an '@' and a domainname containing at least one '.'; this allows you to use an email address as someone's username. Reponames can contain '/' characters; this allows you to put your repos in a tree-structure for convenience. * There are no continuation lines. ACCESS RULES ------------ This section is mostly 'by example'. Gitolite's access rules are very powerful. The simplest use was already shown above. Here is a slightly more detailed example: repo foo RW+ = alice - master = bob - refs/tags/v[0-9] = bob RW = bob RW refs/tags/v[0-9] = carol R = dave For clones and fetches, as long as the user is listed with an R, RW or RW+ in at least one rule, he is allowed to read the repo. For pushes, rules are processed in sequence until a rule is found where the user, the permission (see note 1), and the refex (note 2) *all* match. At that point, if the permission on the matched rule was '-', the push is denied, otherwise it is allowed. If no rule matches, the push is denied. Note 1: permission matching: * a permission of RW matches only a fast-forward push or create * a permission of RW+ matches any type of push * a permission of '-' matches any type of push Note 2: refex matching: (refex = optional regex to match the ref being pushed) * an empty refex is treated as 'refs/.*' * a refex that does not start with 'refs/' is prefixed with 'refs/heads/' * finally, a '^' is prefixed * the ref being pushed is matched against this resulting refex With all that background, here's what the example rules say: * alice can do anything to any branch or tag -- create, push, delete, rewind/overwrite etc. * bob can create or fast-forward push any branch whose name does not start with 'master' and create any tag whose name does not start with 'v'+digit. * carol can create tags whose names start with 'v'+digit. * dave can clone/fetch. GROUPS ------ Gitolite allows you to group users or repos for convenience. Here's an example that creates two groups of users: @staff = alice bob carol @interns = ashok repo secret RW = @staff repo foss RW+ = @staff RW = @interns Group lists accumulate. The following two lines have the same effect as the earlier definition of @staff above: @staff = alice bob @staff = carol You can also use group names in other group names: @all-devs = @staff @interns Finally, @all is a special group name that is often convenient to use if you really mean 'all repos' or 'all users'. COMMANDS -------- Users can run certain commands remotely, using ssh. For example: ssh git@host help prints a list of available commands. The most commonly used command is 'info'. All commands respond to a single argument of '-h' with suitable information. If you have shell on the server, you have a lot more commands available to you; try running 'gitolite help'. THE 'rc' FILE -------------- Some of the instructions below may require you to edit the rc file (~/.gitolite.rc on the server). The rc file is perl code, but you do NOT need to know perl to edit it. Just mind the commas, use single quotes unless you know what you're doing, and make sure the brackets and braces stay matched up. GIT-CONFIG ---------- Gitolite lets you set git-config values for individual repos without having to log on to the server and run 'git config' commands: repo foo config hooks.mailinglist = foo-commits@example.tld config hooks.emailprefix = '[foo] ' config foo.bar = '' config foo.baz = **WARNING** The last two syntaxes shown above are the *only* way to *delete* a config variable once you have added it. Merely removing it from the conf file will *not* delete it from the repo.git/config file. **SECURITY NOTE** Some git-config keys allow arbitrary code to be run on the server. If all of your gitolite admins already have shell access to the server account hosting it, you can edit the rc file (~/.gitolite.rc) on the server, and change the GIT_CONFIG_KEYS line to look like this: GIT_CONFIG_KEYS => '.*', Otherwise, give it a space-separated list of regular expressions that define what git-config keys are allowed. For example, this one allows only variables whose names start with 'gitweb' or with 'gc' to be defined: GIT_CONFIG_KEYS => 'gitweb\..* gc\..*', GIT-DAEMON ---------- Gitolite creates the 'git-daemon-export-ok' file for any repo that is readable by a special user called 'daemon', like so: repo foo R = daemon GITWEB ------ Any repo that is readable by a special user called 'gitweb' will be added to the projects.list file. repo foo R = gitweb Or you can set one or more of the following config variables instead: repo foo config gitweb.owner = some person's name config gitweb.description = some description config gitweb.category = some category **NOTE** You will probably need to change the UMASK in the rc file from the default (0077) to 0027 and add whatever user your gitweb is running as to the 'git' group. After that, you need to run a one-time 'chmod -R' on the already created files and directories. ------------------------------------------------------------------------ MIGRATING FROM v2 ----------------- This section describes how to migrate a basic install of v2 to v3. However, if you have used any of the following features: * any non-default settings in the rc file * NAME/ rules * subconf and delegation * mirroring * wild repos (user-created repos) * any custom hooks of your own you should go through the full set of migration instructions at http://gitolite.com/gitolite/migr.html The steps to follow to migrate a simple v2 setup to v3 are as follows: 0. take a backup :-) 1. remove old gitolite 1.1 Remove (or rename) * the directories named in the rc variables GL_PACKAGE_CONF and GL_PACKAGE_HOOKS (look in ~/.gitolite.rc) * ~/.gitolite.rc * the gitolite v2 code, whose location you can find in the "command=" parameter in any of the gitolite keys in ~/.ssh/authorized_keys * ~/.gitolite (preserve ~/.gitolite/logs if you wish) 1.2 Edit ~/.ssh/authorized_keys and delete all lines pertaining to gitolite (they will have a "command=" option pointing to gl-auth-command) 1.3 Clone ~/repositories/gitolite-admin.git to some safe location on the same server. NOTE: please clone using the file system directly, not via ssh. 1.4 Delete ~/repositories/gitolite-admin.git (the repo you just cloned). NOTE: DO NOT delete any other repo in ~/repositories. Leave them all as they are. 2. install gitolite as normal. It doesn't matter what pubkey you use in the "gitolite setup" step; in fact you may even choose to just run "gitolite setup -a admin". The admin repo created in this step will get wiped out in the next step anyway. 3. go to the clone you made in step 1.3 and run 'gitolite push -f'. NOTE: that is 'gitolite push -f', not 'git push -f' :-) ------------------------------------------------------------------------ CONTACT AND SUPPORT ------------------- Mailing list for support and general discussion: gitolite@googlegroups.com subscribe address: gitolite+subscribe@googlegroups.com Mailing list for announcements and notices: subscribe address: gitolite-announce+subscribe@googlegroups.com IRC: #git and #gitolite on freenode. Note that I live in India (UTC+0530 time zone). Author: sitaramc@gmail.com, but please DO NOT use this for general support questions. Subscribe to the list and ask there instead. LICENSE ------- The gitolite *code* is released under GPL v2. See COPYING for details. This documentation, which is part of the source code repository, is provided under a Creative Commons Attribution-ShareAlike 3.0 Unported License -- see http://creativecommons.org/licenses/by-sa/3.0/ gitolite3-3.6.1/check-g2-compat000077500000000000000000000071101241446647300162300ustar00rootroot00000000000000#!/usr/bin/perl use Cwd; my $h = $ENV{HOME}; my $rc = "$h/.gitolite.rc"; my %count; intro(); msg( FATAL => "no rc file found; do you even *have* g2 running?" ) if not -f $rc; do $rc; unless ( $return = do $rc ) { msg( FATAL => "couldn't parse $rc: $@" ) if $@; msg( FATAL => "couldn't do $rc: $!" ) unless defined $return; msg( WARNING => "couldn't run $rc" ) unless $return; } print "checking rc file...\n"; rc_basic(); rest_of_rc(); print "\n"; print "checking conf file(s)...\n"; conf(); print "\n"; print "checking repos...\n"; repo(); print "\n"; print "...all done...\n"; # ---------------------------------------------------------------------- sub intro { msg( INFO => "This program only checks for uses that make the new g3 completely unusable" ); msg( '' => "or that might end up giving *more* access to someone if migrated as-is." ); msg( '' => "It does NOT attempt to catch all the differences described in the docs." ); msg( '', '' ); msg( INFO => "'see docs' usually means the pre-migration checklist in" ); msg( '', => "'g2migr.html'; to get there, start from the main migration" ); msg( '', => "page at http://gitolite.com/gitolite/migr.html" ); msg( '', '' ); } sub rc_basic { msg( FATAL => "GL_ADMINDIR in the wrong place -- aborting; see docs" ) if $GL_ADMINDIR ne "$h/.gitolite"; msg( NOTE => "GL_ADMINDIR is in the right place; assuming you did not mess with" ); msg( '', "GL_CONF, GL_LOGT, GL_KEYDIR, and GL_CONF_COMPILED" ); msg( FATAL => "REPO_BASE in the wrong place -- aborting; see docs" ) if $REPO_BASE ne "$h/repositories" and $REPO_BASE ne "repositories"; # ( abs or rel both ok) } sub rest_of_rc { msg( SEVERE => "GIT_PATH found; see docs" ) if $GIT_PATH; msg( SEVERE => "GL_ALL_INCLUDES_SPECIAL found; see docs" ) if $GL_ALL_INCLUDES_SPECIAL; msg( SEVERE => "GL_NO_CREATE_REPOS not yet implemented" ) if $GL_NO_CREATE_REPOS; msg( SEVERE => "rsync not yet implemented" ) if $RSYNC_BASE; msg( WARNING => "ADMIN_POST_UPDATE_CHAINS_TO found; see docs" ) if $ADMIN_POST_UPDATE_CHAINS_TO; msg( WARNING => "GL_NO_DAEMON_NO_GITWEB found; see docs" ) if $GL_NO_DAEMON_NO_GITWEB; msg( WARNING => "GL_NO_SETUP_AUTHKEYS found; see docs" ) if $GL_NO_SETUP_AUTHKEYS; msg( WARNING => "UPDATE_CHAINS_TO found; see docs" ) if $UPDATE_CHAINS_TO; msg( WARNING => "GL_ADC_PATH found; see docs" ) if $GL_ADC_PATH; msg( WARNING => "non-default GL_WILDREPOS_PERM_CATS found" ) if $GL_WILDREPOS_PERM_CATS ne 'READERS WRITERS'; } sub conf { chdir($h); chdir($GL_ADMINDIR); my $conf = `find . -name "*.conf" | xargs cat`; msg( "SEVERE", "NAME rules; see docs" ) if $conf =~ m(NAME/); msg( "SEVERE", "subconf command in admin repo; see docs" ) if $conf =~ m(NAME/conf/fragments); msg( "SEVERE", "mirroring used; see docs" ) if $conf =~ m(config +gitolite\.mirror\.); } sub repo { chdir($h); chdir($REPO_BASE); my @creater = `find . -name gl-creater`; if (@creater) { msg( WARNING => "found " . scalar(@creater) . " gl-creater files; see docs" ); } my @perms = `find . -name gl-perms | xargs egrep -l -w R\\|RW`; if (@perms) { msg( WARNING => "found " . scalar(@perms) . " gl-perms files with R or RW; see docs" ); } } sub msg { my ( $type, $text ) = @_; print "$type" if $type; print "\t$text\n"; exit 1 if $type eq 'FATAL'; $count{$type}++ if $type; } gitolite3-3.6.1/contrib/000077500000000000000000000000001241446647300150775ustar00rootroot00000000000000gitolite3-3.6.1/contrib/commands/000077500000000000000000000000001241446647300167005ustar00rootroot00000000000000gitolite3-3.6.1/contrib/commands/ukm000077500000000000000000000715321241446647300174320ustar00rootroot00000000000000#!/usr/bin/perl use strict; use warnings; use lib $ENV{GL_LIBDIR}; use Gitolite::Rc; use Gitolite::Common; use Gitolite::Easy; =for usage Usage for this command is not that simple. Please read the full documentation in https://github.com/sitaramc/gitolite-doc/blob/master/contrib/ukm.mkd or online at http://gitolite.com/gitolite/ukm.html. =cut usage() if @ARGV and $ARGV[0] eq '-h'; # Terms used in this file. # pubkeypath: the (relative) filename of a public key starting from # gitolite-admin/keydir. Examples: alice.pub, foo/bar/alice.pub, # alice@home.pub, foo/alice@laptop.pub. You get more examples, if you # replace "alice" by "bob@example.com". # userid: computed from a pubkeypath by removing any directory # part, the '.pub' extension and the "old-style" @NAME classifier. # The userid identifies a user in the gitolite.conf file. # keyid: an identifier for a key given on the command line. # If the script is called by one of the super_key_managers, then the # keyid is the pubkeypath without the '.pub' extension. Otherwise it # is the userid for a guest. # The keyid is normalized to lowercase letters. my $rb = $rc{GL_REPO_BASE}; my $ab = $rc{GL_ADMIN_BASE}; # This will be the subdirectory under "keydir" in which the guest # keys will be stored. To prevent denial of service, this directory # should better start with 'zzz'. # The actual value can be set through the GUEST_DIRECTORY resource. # WARNING: If this value is changed you must understand the consequences. # There will be no support if guestkeys_dir is anything else than # 'zzz/guests'. my $guestkeys_dir = 'zzz/guests'; # A guest key cannot have arbitrary names (keyid). Only keys that do *not* # match $forbidden_guest_pattern are allowed. Super-key-managers can add # any keyid. # This is the directory for additional keys of a self key manager. my $selfkeys_dir = 'zzz/self'; # There is no flexibility for selfkeys. One must specify a keyid that # matches the regular expression '^@[a-z0-9]+$'. Note that all keyids # are transformed to lowercase before checking. my $required_self_pattern = qr([a-z0-9]+); my $selfkey_management = 0; # disable selfkey managment # For guest key managers the keyid must pass two tests. # 1) It must match the $required_guest_pattern regular expression. # 2) It must not match the $forbidden_guest_pattern regular expression. # Default for $forbidden_guest_pattern is qr(.), i.e., every keyid is # forbidden, or in other words, only the gitolite-admin can manage keys. # Default for $required_guest_pattern is such that the keyid must look # like an email address, i.e. must have exactly one @ and at least one # dot after the @. # Just setting 'ukm' => 1 in .gitolite.rc only allows the super-key-managers # (i.e., only the gitolite admin(s)) to manage keys. my $required_guest_pattern = qr(^[0-9a-z][-0-9a-z._+]*@[-0-9a-z._+]+[.][-0-9a-z._+]+$); my $forbidden_guest_pattern = qr(.); die "The command 'ukm' is not enabled.\n" if ! $rc{'COMMANDS'}{'ukm'}; my $km = $rc{'UKM_CONFIG'}; if(ref($km) eq 'HASH') { # If not set we only allow keyids that look like emails my $rgp = $rc{'UKM_CONFIG'}{'REQUIRED_GUEST_PATTERN'} || ''; $required_guest_pattern = qr(^($rgp)$) if $rgp; $forbidden_guest_pattern = $rc{'UKM_CONFIG'}{'FORBIDDEN_GUEST_PATTERN'} || $forbidden_guest_pattern; $selfkey_management = $rc{'UKM_CONFIG'}{'SELFKEY_MANAGEMENT'} || 0; } # get the actual userid my $gl_user = $ENV{GL_USER}; my $super_key_manager = is_admin(); # or maybe is_super_admin() ? # save arguments for later my $operation = shift || 'list'; my $keyid = shift || ''; $keyid = lc $keyid; # normalize to lowercase ids my ($zop, $zfp, $zselector, $zuser) = get_pending($gl_user); # The following will only be true if a selfkey manager logs in to # perform a pending operation. my $pending_self = ($zop ne ''); die "You are not a key manager.\n" unless $super_key_manager || $pending_self || in_group('guest-key-managers') || in_group('self-key-managers'); # Let's deal with the pending user first. The only allowed operations # that are to confirm the add operation with the random code # that must be provided via stdin or to undo a pending del operation. if ($pending_self) { pending_user($gl_user, $zop, $zfp, $zselector, $zuser); exit; } my @available_operations = ('list','add','del'); die "unknown ukm subcommand: $operation\n" unless grep {$operation eq $_} @available_operations; # get to the keydir _chdir("$ab/keydir"); # Note that the program warns if it finds a fingerprint that maps to # different userids. my %userids = (); # mapping from fingerprint to userid my %fingerprints = (); # mapping from pubkeypath to fingerprint my %pubkeypaths = (); # mapping from userid to pubkeypaths # note that the result is a list of pubkeypaths # Guest keys are managed by people in the @guest-key-managers group. # They can only add/del keys in the $guestkeys_dir directory. In fact, # the guest key manager $gl_user has only access to keys inside # %guest_pubkeypaths. my %guest_pubkeypaths = (); # mapping from userid to pubkeypath for $gl_user # Self keys are managed by people in the @self-key-managers group. # They can only add/del keys in the $selfkeys_dir directory. In fact, # the self key manager $gl_user has only access to keys inside # %self_pubkeypaths. my %self_pubkeypaths = (); # These are the keys that are managed by a super key manager. my @all_pubkeypaths = `find . -type f -name "*.pub" 2>/dev/null | sort`; for my $pubkeypath (@all_pubkeypaths) { chomp($pubkeypath); my $fp = fingerprint($pubkeypath); $fingerprints{$pubkeypath} = $fp; my $userid = get_userid($pubkeypath); my ($zop, $zfp, $zselector, $zuser) = get_pending($userid); $userid = $zuser if $zop; if (! defined $userids{$fp}) { $userids{$fp} = $userid; } else { warn "key $fp is used for different user ids\n" unless $userids{$fp} eq $userid; } push @{$pubkeypaths{$userid}}, $pubkeypath; if ($pubkeypath =~ m|^./$guestkeys_dir/([^/]+)/[^/]+\.pub$|) { push @{$guest_pubkeypaths{$userid}}, $pubkeypath if $gl_user eq $1; } if ($pubkeypath =~ m|^./$selfkeys_dir/([^/]+)/[^/]+\.pub$|) { push @{$self_pubkeypaths{$userid}}, $pubkeypath if $gl_user eq $1; } } ################################################################### # do stuff according to the operation ################################################################### if ( $operation eq 'list' ) { list_pubkeys(); print "\n\n"; exit; } die "keyid required\n" unless $keyid; die "Not allowed to use '..' in keyid.\n" if $keyid =~ /\.\./; if ( $operation eq 'add' ) { if ($super_key_manager) { add_pubkey($gl_user, "$keyid.pub", safe_stdin()); } elsif (selfselector($keyid)) { add_self($gl_user, $keyid, safe_stdin()); } else { # assert ingroup('guest-key-managers'); add_guest($gl_user, $keyid, safe_stdin()); } } elsif ( $operation eq 'del' ) { if ($super_key_manager) { del_super($gl_user, "$keyid.pub"); } elsif (selfselector($keyid)) { del_self($gl_user, $keyid); } else { # assert ingroup('guest-key-managers'); del_guest($gl_user, $keyid); } } exit; ################################################################### # only function definitions are following ################################################################### # make a temp clone and switch to it our $TEMPDIR; BEGIN { $TEMPDIR = `mktemp -d -t tmp.XXXXXXXXXX`; chomp($TEMPDIR) } END { my $err = $?; `/bin/rm -rf $TEMPDIR`; $? = $err; } sub cd_temp_clone { chomp($TEMPDIR); hushed_git( "clone", "$rb/gitolite-admin.git", "$TEMPDIR/gitolite-admin" ); chdir("$TEMPDIR/gitolite-admin"); my $ip = $ENV{SSH_CONNECTION}; $ip =~ s/ .*//; my ($zop, $zfp, $zselector, $zuser) = get_pending($ENV{GL_USER}); my $email = $zuser; $email .= '@' . $ip unless $email =~ m(@); my $name = $zop ? "\@$zselector" : $zuser; # Record the keymanager in the gitolite-admin repo as author of the change. hushed_git( "config", "user.email", "$email" ); hushed_git( "config", "user.name", "'$name from $ip'" ); } # compute the fingerprint from the full path of a pubkey file sub fingerprint { my $fp = `ssh-keygen -l -f $_[0]`; die "does not seem to be a valid pubkey\n" unless $fp =~ /(([0-9a-f]+:)+[0-9a-f]+) /i; return $1; } # Read one line from STDIN and return it. # If no data is available on STDIN after one second, the empty string # is returned. # If there is more than one line or there was an error in reading, the # function dies. sub safe_stdin { use IO::Select; my $s=IO::Select->new(); $s->add(\*STDIN); return '' unless $s->can_read(1); my $data; my $ret = read STDIN, $data, 4096; # current pubkeys are approx 400 bytes so we go a little overboard die "could not read pubkey data" . ( defined($ret) ? "" : ": $!" ) . "\n" unless $ret; die "pubkey data seems to have more than one line\n" if $data =~ /\n./; return $data; } # call git, be quiet sub hushed_git { system("git " . join(" ", @_) . ">/dev/null 2>/dev/null"); } # Extract the userid from the full path of the pubkey file (relative # to keydir/ and including the '.pub' extension. sub get_userid { my ($u) = @_; # filename of pubkey relative to keydir/. $u =~ s(.*/)(); # foo/bar/baz.pub -> baz.pub $u =~ s/(\@[^.]+)?\.pub$//; # baz.pub, baz@home.pub -> baz return $u; } # Extract the @selector part from the full path of the pubkey file # (relative to keydir/ and including the '.pub' extension). # If there is no @selector part, the empty string is returned. # We also correctly extract the selector part from pending keys. sub get_selector { my ($u) = @_; # filename of pubkey relative to keydir/. $u =~ s(.*/)(); # foo/bar/baz.pub -> baz.pub $u =~ s(\.pub$)(); # baz@home.pub -> baz@home return $1 if $u =~ m/.\@($required_self_pattern)$/; # baz@home -> home my ($zop, $zfp, $zselector, $zuser) = get_pending($u); # If $u was not a pending key, then $zselector is the empty string. return $zselector; } # Extract fingerprint, operation, selector, and true userid from a # pending userid. sub get_pending { my ($gl_user) = @_; return ($1, $2, $3, $4) if ($gl_user=~/^zzz-(...)-([0-9a-f]{32})-($required_self_pattern)-(.*)/); return ('', '', '', $gl_user) } # multiple / and are simplified to one / and the path is made relative sub sanitize_pubkeypath { my ($pubkeypath) = @_; $pubkeypath =~ s|//|/|g; # normalize path $pubkeypath =~ s,\./,,g; # remove './' from path return './'.$pubkeypath; # Don't allow absolute paths. } # This function is only relavant for guest key managers. # It returns true if the pattern is OK and false otherwise. sub required_guest_keyid { my ($_) = @_; /$required_guest_pattern/ and ! /$forbidden_guest_pattern/; } # The function takes a $keyid as input and returns the keyid with the # initial @ stripped if everything is fine. It aborts with an error if # selfkey management is not enabled or the function is called for a # non-self-key-manager. # If the required selfkey pattern is not matched, it returns an empty string. # Thus the function can be used to check whether a given keyid is a # proper selfkeyid. sub selfselector { my ($keyid) = @_; return '' unless $keyid =~ m(^\@($required_self_pattern)$); $keyid = $1; die "selfkey management is not enabled\n" unless $selfkey_management; die "You are not a selfkey manager.\n" if ! in_group('self-key-managers'); return $keyid; } # Return the number of characters reserved for the userid field. sub userid_width { my ($paths) = @_; my (%pkpaths) = %{$paths}; my (@userid_lengths) = sort {$a <=> $b} (map {length($_)} keys %pkpaths); @userid_lengths ? $userid_lengths[-1] : 0; } # List the keys given by a reference to a hash. # The regular expression $re is used to remove the initial part of the # keyid and replace it by what is matched inside the parentheses. # $format and $width are used for pretty printing sub list_keys { my ($paths, $tokeyid, $format, $width) = @_; my (%pkpaths) = %{$paths}; for my $userid (sort keys %pkpaths) { for my $pubkeypath (sort @{$pkpaths{$userid}}) { my $fp = $fingerprints{$pubkeypath}; my $userid = $userids{$fp}; my $keyid = &{$tokeyid}($pubkeypath); printf $format,$fp,$userid,$width+1-length($userid),"",$keyid if ($super_key_manager || required_guest_keyid($keyid) || $keyid=~m(^\@)); } } } # Turn a pubkeypath into a keyid for super-key-managers, guest-keys, # and self-keys. sub superkeyid { my ($keyid) = @_; $keyid =~ s(\.pub$)(); $keyid =~ s(^\./)(); return $keyid; } sub guestkeyid { my ($keyid) = @_; $keyid =~ s(\.pub$)(); $keyid =~ s(^.*/)(); return $keyid; } sub selfkeyid { my ($keyid) = @_; $keyid =~ s(\.pub$)(); $keyid =~ s(^.*/)(); my ($zop, $zfp, $zselector, $zuser) = get_pending($keyid); return "\@$zselector (pending $zop)" if $zop; $keyid =~ s(.*@)(@); return $keyid; } ################################################################### # List public keys managed by the respective user. # The fingerprints, userids and keyids are printed. # keyids are shown in a form that can be used for add and del # subcommands. sub list_pubkeys { print "Hello $gl_user, you manage the following keys:\n"; my $format = "%-47s %s%*s%s\n"; my $width = 0; if ($super_key_manager) { $width = userid_width(\%pubkeypaths); $width = 6 if $width < 6; # length("userid")==6 printf $format, "fingerprint", "userid", ($width-5), "", "keyid"; list_keys(\%pubkeypaths, , \&superkeyid, $format, $width); } else { my $widths = $selfkey_management?userid_width(\%self_pubkeypaths):0; my $widthg = userid_width(\%guest_pubkeypaths); $width = $widths > $widthg ? $widths : $widthg; # maximum width return unless $width; # there are no keys $width = 6 if $width < 6; # length("userid")==6 printf $format, "fingerprint", "userid", ($width-5), "", "keyid"; list_keys(\%self_pubkeypaths, \&selfkeyid, $format, $width) if $selfkey_management; list_keys(\%guest_pubkeypaths, \&guestkeyid, $format, $width); } } ################################################################### # Add a public key for the user $gl_user. # $pubkeypath is the place where the new key will be stored. # If the file or its fingerprint already exists, the operation is # rejected. sub add_pubkey { my ( $gl_user, $pubkeypath, $keymaterial ) = @_; if(! $keymaterial) { print STDERR "Please supply the new key on STDIN.\n"; print STDERR "Try something like this:\n"; print STDERR "cat FOO.pub | ssh GIT\@GITOLITESERVER ukm add KEYID\n"; die "missing public key data\n"; } # clean pubkeypath a bit $pubkeypath = sanitize_pubkeypath($pubkeypath); # Check that there is not yet something there already. die "cannot override existing key\n" if $fingerprints{$pubkeypath}; my $userid = get_userid($pubkeypath); # Super key managers shouldn't be able to add a that leads to # either an empty userid or to a userid that starts with @. # # To avoid confusion, all keyids for super key managers must be in # a full path format. Having a public key of the form # gitolite-admin/keydir/@foo.pub might be confusing and might lead # to other problems elsewhere. die "cannot add key that starts with \@\n" if (!$userid) || $userid=~/^@/; cd_temp_clone(); _chdir("keydir"); $pubkeypath =~ m((.*)/); # get the directory part _mkdir($1); _print($pubkeypath, $keymaterial); my $fp = fingerprint($pubkeypath); # Maybe we are adding a selfkey. my ($zop, $zfp, $zselector, $zuser) = get_pending($userid); my $user = $zop ? "$zuser\@$zselector" : $userid; $userid = $zuser; # Check that there isn't a key with the same fingerprint under a # different userid. if (defined $userids{$fp}) { if ($userid ne $userids{$fp}) { print STDERR "Found $fp $userids{$fp}\n" if $super_key_manager; print STDERR "Same key is already available under another userid.\n"; die "cannot add key\n"; } elsif ($zop) { # Because of the way a key is confirmed with ukm, it is # impossible to confirm the initial key of the user as a # new selfkey. (It will lead to the function list_pubkeys # instead of pending_user_add, because the gl_user will # not be that of a pending user.) To avoid confusion, we, # therefore, forbid to add the user's initial key # altogether. # In fact, we here also forbid to add any key for that # user that is already in the system. die "You cannot add a key that already belongs to you.\n"; } } else {# this fingerprint does not yet exist my @paths = @{$pubkeypaths{$userid}} if defined $pubkeypaths{$userid}; if (@paths) {# there are already keys for $userid if (grep {$pubkeypath eq $_} @paths) { print STDERR "The keyid is already present. Nothing changed.\n"; } elsif ($super_key_manager) { # It's OK to add new selfkeys, but here we are in the case # of adding multiple keys for guests. That is forbidden. print STDERR "Adding new public key for $userid.\n"; } elsif ($pubkeypath =~ m(^\./$guestkeys_dir/)) { # Arriving here means we are about to add a *new* # guest key, because the fingerprint is not yet # existing. This would be for an already existing # userid (added by another guest key manager). Since # that effectively means to (silently) add an # additional key for an existing user, it must be # forbidden. die "cannot add another public key for an existing user\n"; } } } exit if (`git status -s` eq ''); # OK to add identical keys twice hushed_git( "add", "." ) and die "git add failed\n"; hushed_git( "commit", "-m", "'ukm add $gl_user $userid\n\n$fp'" ) and die "git commit failed\n"; system("gitolite push >/dev/null 2>/dev/null") and die "git push failed\n"; } # Guest key managers should not be allowed to add directories or # multiple keys via the @domain mechanism, since this might allow # another guest key manager to give an attacker access to another # user's repositories. # # Example: Alice adds bob.pub for bob@example.org. David adds eve.pub # (where only Eve but not Bob has the private key) under the keyid # bob@example.org@foo. This basically gives Eve the same rights as # Bob. sub add_guest { my ( $gl_user, $keyid, $keymaterial ) = @_; die "keyid not allowed: '$keyid'\n" if $keyid =~ m(@.*@) or $keyid =~ m(/) or !required_guest_keyid($keyid); add_pubkey($gl_user, "$guestkeys_dir/$gl_user/$keyid.pub", $keymaterial); } # Add a new selfkey for user $gl_user. sub add_self { my ( $gl_user, $keyid, $keymaterial ) = @_; my $selector = ""; $selector = selfselector($keyid); # might return empty string die "keyid not allowed: $keyid\n" unless $selector; # Check that the new selector is not already in use even not in a # pending state. die "keyid already in use: $keyid\n" if grep {selfkeyid($_)=~/^\@$selector( .*)?$/} @{$self_pubkeypaths{$gl_user}}; # generate new pubkey create fingerprint system("ssh-keygen -N '' -q -f \"$TEMPDIR/session\" -C $gl_user"); my $sessionfp = fingerprint("$TEMPDIR/session.pub"); $sessionfp =~ s/://g; my $user = "zzz-add-$sessionfp-$selector-$gl_user"; add_pubkey($gl_user, "$selfkeys_dir/$gl_user/$user.pub", $keymaterial); print `cat "$TEMPDIR/session.pub"`; } ################################################################### # Delete a key of user $gl_user. sub del_pubkey { my ($gl_user, $pubkeypath) = @_; $pubkeypath = sanitize_pubkeypath($pubkeypath); my $fp = $fingerprints{$pubkeypath}; die "key not found\n" unless $fp; cd_temp_clone(); chdir("keydir"); hushed_git( "rm", "$pubkeypath" ) and die "git rm failed\n"; my $userid = get_userid($pubkeypath); hushed_git( "commit", "-m", "'ukm del $gl_user $userid\n\n$fp'" ) and die "git commit failed\n"; system("gitolite push >/dev/null 2>/dev/null") and die "git push failed\n"; } # $gl_user is a super key manager. This function aborts if the # superkey manager tries to remove his last key. sub del_super { my ($gl_user, $pubkeypath) = @_; $pubkeypath = sanitize_pubkeypath($pubkeypath); die "You are not managing the key $keyid.\n" unless grep {$_ eq $pubkeypath} @all_pubkeypaths; my $userid = get_userid($pubkeypath); if ($gl_user eq $userid) { my @paths = @{$pubkeypaths{$userid}}; die "You cannot delete your last key.\n" if scalar(grep {$userid eq get_userid($_)} @paths)<2; } del_pubkey($gl_user, $pubkeypath); } sub del_guest { my ($gl_user, $keyid) = @_; my $pubkeypath = sanitize_pubkeypath("$guestkeys_dir/$gl_user/$keyid.pub"); my $userid = get_userid($pubkeypath); # Check whether $gl_user actually manages $keyid. my @paths = (); @paths = @{$guest_pubkeypaths{$userid}} if defined $guest_pubkeypaths{$userid}; die "You are not managing the key $keyid.\n" unless grep {$_ eq $pubkeypath} @paths; del_pubkey($gl_user, $pubkeypath); } # Delete a selfkey of $gl_user. The first delete is a preparation of # the deletion and only a second call will actually delete the key. If # the second call is done with the key that is scheduled for deletion, # it is basically undoing the previous del call. This last case is # handled in function pending_user_del. sub del_self { my ($gl_user, $keyid) = @_; my $selector = selfselector($keyid); # might return empty string die "keyid not allowed: '$keyid'\n" unless $selector; # Does $gl_user actually manage that keyid? # All (non-pending) selfkeys have an @selector part in their pubkeypath. my @paths = @{$self_pubkeypaths{$gl_user}}; die "You are not managing the key $keyid.\n" unless grep {$selector eq get_selector($_)} @paths; cd_temp_clone(); _chdir("keydir"); my $fp = ''; # Is it the first or the second del call? It's the second call, if # there is a scheduled-for-deletion or scheduled-for-addition # selfkey which has the given keyid as a selector part. @paths = grep { my ($zop, $zfp, $zselector, $zuser) = get_pending(get_userid($_)); $zselector eq $selector } @paths; if (@paths) {# start actual deletion of the key (second call) my $pubkeypath = $paths[0]; $fp = fingerprint($pubkeypath); my ($zop, $zf, $zs, $zu) = get_pending(get_userid($pubkeypath)); $zop = $zop eq 'add' ? 'undo-add' : 'confirm-del'; hushed_git("rm", "$pubkeypath") and die "git rm failed\n"; hushed_git("commit", "-m", "'ukm $zop $gl_user\@$selector\n\n$fp'") and die "git commit failed\n"; system("gitolite push >/dev/null 2>/dev/null") and die "git push failed\n"; print STDERR "pending keyid deleted: \@$selector\n"; return; } my $oldpubkeypath = "$selfkeys_dir/$gl_user/$gl_user\@$selector.pub"; # generate new pubkey and create fingerprint to get a random number system("ssh-keygen -N '' -q -f \"$TEMPDIR/session\" -C $gl_user"); my $sessionfp = fingerprint("$TEMPDIR/session.pub"); $sessionfp =~ s/://g; my $user = "zzz-del-$sessionfp-$selector-$gl_user"; my $newpubkeypath = "$selfkeys_dir/$gl_user/$user.pub"; # A key for gitolite access that is in authorized_keys and not # existing in the expected place under keydir/ should actually not # happen, but one never knows. die "key not available\n" unless -r $oldpubkeypath; # For some strange reason the target key already exists. die "cannot override existing key\n" if -e $newpubkeypath; $fp = fingerprint($oldpubkeypath); print STDERR "prepare deletion of key \@$selector\n"; hushed_git("mv", "$oldpubkeypath", "$newpubkeypath") and die "git mv failed\n"; hushed_git("commit", "-m", "'ukm prepare-del $gl_user\@$selector\n\n$fp'") and die "git commit failed\n"; system("gitolite push >/dev/null 2>/dev/null") and die "git push failed\n"; } ################################################################### # Adding a selfkey should be done as follows. # # cat newkey.pub | ssh git@host ukm add @selector > session # cat session | ssh -i newkey git@host ukm # # The provided random data will come from a newly generated ssh key # whose fingerprint will be stored in $gl_user. So we compute the # fingerprint of the data that is given to us. If it doesn't match the # fingerprint, then something went wrong and the confirm operation is # forbidden, in fact, the pending key will be removed from the system. sub pending_user_add { my ($gl_user, $zfp, $zselector, $zuser) = @_; my $oldpubkeypath = "$selfkeys_dir/$zuser/$gl_user.pub"; my $newpubkeypath = "$selfkeys_dir/$zuser/$zuser\@$zselector.pub"; # A key for gitolite access that is in authorized_keys and not # existing in the expected place under keydir/ should actually not # happen, but one never knows. die "key not available\n" unless -r $oldpubkeypath; my $keymaterial = safe_stdin(); # If there is no keymaterial (which corresponds to a session key # for the confirm-add operation), logging in to this key, removes # it from the system. my $session_key_not_provided = ''; if (!$keymaterial) { $session_key_not_provided = "missing session key"; } else { _print("$TEMPDIR/session.pub", $keymaterial); my $sessionfp = fingerprint("$TEMPDIR/session.pub"); $sessionfp =~ s/://g; $session_key_not_provided = "session key not accepted" unless ($zfp eq $sessionfp) } my $fp = fingerprint($oldpubkeypath); if ($session_key_not_provided) { print STDERR "$session_key_not_provided\n"; print STDERR "pending keyid deleted: \@$zselector\n"; hushed_git("rm", "$oldpubkeypath") and die "git rm failed\n"; hushed_git("commit", "-m", "'ukm del $zuser\@$zselector\n\n$fp'") and die "git commit failed\n"; system("gitolite push >/dev/null 2>/dev/null") and die "git push failed\n"; return; } # For some strange reason the target key already exists. die "cannot override existing key\n" if -e $newpubkeypath; print STDERR "pending keyid added: \@$zselector\n"; hushed_git("mv", "$oldpubkeypath", "$newpubkeypath") and die "git mv failed\n"; hushed_git("commit", "-m", "'ukm confirm-add $zuser\@$zselector\n\n$fp'") and die "git commit failed\n"; system("gitolite push >/dev/null 2>/dev/null") and die "git push failed\n"; } # To delete a key, one must first bring the key into a pending state # and then truely delete it with another key. In case, the login # happens with the pending key (implemented below), it means that the # delete operation has to be undone. sub pending_user_del { my ($gl_user, $zfp, $zselector, $zuser) = @_; my $oldpubkeypath = "$selfkeys_dir/$zuser/$gl_user.pub"; my $newpubkeypath = "$selfkeys_dir/$zuser/$zuser\@$zselector.pub"; print STDERR "undo pending deletion of keyid \@$zselector\n"; # A key for gitolite access that is in authorized_keys and not # existing in the expected place under keydir/ should actually not # happen, but one never knows. die "key not available\n" unless -r $oldpubkeypath; # For some strange reason the target key already exists. die "cannot override existing key\n" if -e $newpubkeypath; my $fp = fingerprint($oldpubkeypath); hushed_git("mv", "$oldpubkeypath", "$newpubkeypath") and die "git mv failed\n"; hushed_git("commit", "-m", "'ukm undo-del $zuser\@$zselector\n\n$fp'") and die "git commit failed\n"; } # A user whose key is in pending state cannot do much. In fact, # logging in as such a user simply takes back the "bringing into # pending state", i.e. a key scheduled for adding is remove and a key # scheduled for deletion is brought back into its properly added state. sub pending_user { my ($gl_user, $zop, $zfp, $zselector, $zuser) = @_; cd_temp_clone(); _chdir("keydir"); if ($zop eq 'add') { pending_user_add($gl_user, $zfp, $zselector, $zuser); } elsif ($zop eq 'del') { pending_user_del($gl_user, $zfp, $zselector, $zuser); } else { die "unknown operation\n"; } system("gitolite push >/dev/null 2>/dev/null") and die "git push failed\n"; } gitolite3-3.6.1/contrib/t/000077500000000000000000000000001241446647300153425ustar00rootroot00000000000000gitolite3-3.6.1/contrib/t/ukm.t000066400000000000000000000415221241446647300163270ustar00rootroot00000000000000#!/usr/bin/perl # Call like this: # TSH_VERBOSE=1 TSH_ERREXIT=1 HARNESS_ACTIVE=1 GITOLITE_TEST=y prove t/ukm.t use strict; use warnings; # this is hardcoded; change it if needed use lib "src/lib"; use Gitolite::Common; use Gitolite::Test; # basic tests using ssh # ---------------------------------------------------------------------- my $bd = `gitolite query-rc -n GL_BINDIR`; my $h = $ENV{HOME}; my $ab = `gitolite query-rc -n GL_ADMIN_BASE`; my $pd = "$bd/../t/keys"; # source for pubkeys umask 0077; _mkdir( "$h/.ssh", 0700 ) if not -d "$h/.ssh"; try "plan 204"; # Reset everything. # Only admin and u1, u2, and u3 keys are available initially # Keys u4, u5, and u6 are used as guests later. # For easy access, we put the keys into ~/.ssh/, though. try " rm -f $h/.ssh/authorized_keys; ok or die 1 cp $pd/u[1-6]* $h/.ssh; ok or die 2 cp $pd/admin* $h/.ssh; ok or die 3 cp $pd/config $h/.ssh; ok or die 4 cat $h/.ssh/config perl s/%USER/$ENV{USER}/ put $h/.ssh/config mkdir $ab/keydir; ok or die 5 cp $pd/u[1-3].pub $ab/keydir; ok or die 6 cp $pd/admin.pub $ab/keydir; ok or die 7 "; # Put the keys into ~/.ssh/authorized_keys system("gitolite ../triggers/post-compile/ssh-authkeys"); # enable user key management in a simple form. # Guest key managers can add keyids looking like email addresses, but # cannot add emails containing example.com or hemmecke.org. system("sed -i \"s/.*ENABLE =>.*/'UKM_CONFIG'=>{'FORBIDDEN_GUEST_PATTERN'=>'example.com|hemmecke.org'}, ENABLE => ['ukm',/\" $h/.gitolite.rc"); # super-key-managers can add/del any key # super-key-managers should in fact agree with people having write # access to gitolite-admin repo. # guest-key-managers can add/del guest keys confreset; confadd ' @guest-key-managers = u2 u3 @creators = u2 u3 repo pub/CREATOR/..* C = @creators RW+ = CREATOR RW = WRITERS R = READERS '; # Populate the gitolite-admin/keydir in the same way as it was used for # the initialization of .ssh/authorized_keys above. try " mkdir keydir; ok or die 8 cp $pd/u[1-3].pub keydir; ok or die 9; cp $pd/admin.pub keydir; ok or die 10; git add conf keydir; ok git commit -m ukm; ok; /master.* ukm/ "; # Activate new config data. try "PUSH admin; ok; gsh; /master -> master/; !/FATAL/" or die text(); # Check whether the above setup yields the expected behavior for ukm. # The admin is super-key-manager, thus can manage every key. try " ssh admin ukm; ok; /Hello admin, you manage the following keys:/ / admin +admin/ / u1 +u1/ / u2 +u2/ / u3 +u3/ "; # u1 isn't a key manager, so shouldn't be above to manage keys. try "ssh u1 ukm; !ok; /FATAL: You are not a key manager./"; # u2 and u3 are guest key managers, but don't yet manage any key. try "ssh u2 ukm; ok"; cmp "Hello u2, you manage the following keys:\n\n\n"; try "ssh u3 ukm; ok"; cmp "Hello u3, you manage the following keys:\n\n\n"; ################################################################### # Unknows subkommands abort ukm. try "ssh u2 ukm fake; !ok; /FATAL: unknown ukm subcommand: fake/"; ################################################################### # Addition of keys. # If no data is provided on stdin, we don't block, but rather timeout # after one second and abort the program. try "ssh u2 ukm add u4\@example.org; !ok; /FATAL: missing public key data/"; # If no keyid is given, we cannot add a key. try "ssh u2 ukm add; !ok; /FATAL: keyid required/"; try " DEF ADD = cat $pd/%1.pub|ssh %2 ukm add %3 DEF ADDOK = ADD %1 %2 %3; ok DEF ADDNOK = ADD %1 %2 %3; !ok DEF FP = ADDNOK u4 u2 %1 DEF FORBIDDEN_PATTERN = FP %1; /FATAL: keyid not allowed:/ "; # Neither a guest key manager nor a super key manager can add keys that have # double dot in their keyid. This is hardcoded to forbid paths with .. in it. try " ADDNOK u4 u2 u4\@hemmecke..org; /Not allowed to use '..' in keyid./ ADDNOK u4 admin u4\@hemmecke..org; /Not allowed to use '..' in keyid./ ADDNOK u4 admin ./../.myshrc; /Not allowed to use '..' in keyid./ "; # guest-key-managers can only add keys that look like emails. try " FORBIDDEN_PATTERN u4 FORBIDDEN_PATTERN u4\@example FORBIDDEN_PATTERN u4\@foo\@example.org # No support for 'old style' multiple keys. FORBIDDEN_PATTERN u4\@example.org\@foo # No path delimiter in keyid FORBIDDEN_PATTERN foo/u4\@example.org # Certain specific domains listed in FORBIDDEN_GUEST_PATTERN are forbidden. # Note that also u4\@example-com would be rejected, because MYDOMAIN # contains a regular expression --> I don't care. FORBIDDEN_PATTERN u4\@example.com FORBIDDEN_PATTERN u4\@hemmecke.org "; # Accept one guest key. try "ADDOK u4 u2 u4\@example.org"; try "ssh u2 ukm; ok; /Hello u2, you manage the following keys:/ / u4\@example.org *u4\@example.org/"; # Various ways how a key must be rejected. try " # Cannot add the same key again. ADDNOK u4 u2 u4\@example.org; /FATAL: cannot override existing key/ # u2 can also not add u4.pub under another keyid ADDNOK u4 u2 u4\@example.net; /FATAL: cannot add key/ /Same key is already available under another userid./ # u2 can also not add another key under the same keyid. ADDNOK u5 u2 u4\@example.org; /FATAL: cannot override existing key/ # Also u3 cannot not add another key under the same keyid. ADDNOK u5 u3 u4\@example.org /FATAL: cannot add another public key for an existing user/ # And u3 cannot not add u4.pub under another keyid. ADDNOK u4 u3 u4\@example.net; /FATAL: cannot add key/ /Same key is already available under another userid./ # Not even the admin can add the same key u4 under a different userid. ADDNOK u4 admin u4\@example.net; /FATAL: cannot add key/ /Same key is already available under another userid./ /Found .* u4\@example.org/ # Super key managers cannot add keys that start with @. # We don't care about @ in the dirname, though. ADDNOK u4 admin foo/\@ex.net; /FATAL: cannot add key that starts with \@/ ADDNOK u4 admin foo/\@ex; /FATAL: cannot add key that starts with \@/ ADDNOK u4 admin \@ex.net; /FATAL: cannot add key that starts with \@/ ADDNOK u4 admin \@ex; /FATAL: cannot add key that starts with \@/ "; # But u3 can add u4.pub under the same keyid. try "ADDOK u4 u3 u4\@example.org"; try "ssh u3 ukm; ok; /Hello u3, you manage the following keys:/ / u4\@example.org *u4\@example.org/"; # The admin can add multiple keys for the same userid. try " ADDOK u5 admin u4\@example.org ADDOK u5 admin u4\@example.org\@home ADDOK u5 admin laptop/u4\@example.org ADDOK u5 admin laptop/u4\@example.org\@home "; # And admin can also do this for other guest key managers. Note, # however, that the gitolite-admin must be told where the # GUEST_DIRECTORY is. But he/she could find out by cloning the # gitolite-admin repository and adding the same key directly. try " ADDOK u5 admin zzz/guests/u2/u4\@example.org\@foo ADDOK u6 admin zzz/guests/u3/u6\@example.org "; try "ssh admin ukm; ok"; cmp "Hello admin, you manage the following keys: fingerprint userid keyid a4:d1:11:1d:25:5c:55:9b:5f:91:37:0e:44:a5:a5:f2 admin admin 00:2c:1f:dd:a3:76:5a:1e:c4:3c:01:15:65:19:a5:2e u1 u1 69:6f:b5:8a:f5:7b:d8:40:ce:94:09:a2:b8:95:79:5b u2 u2 26:4b:20:24:98:a4:e4:a5:b9:97:76:9a:15:92:27:2d u3 u3 78:cf:7e:2b:bf:18:58:54:23:cc:4b:3d:7e:f4:63:79 u4\@example.org laptop/u4\@example.org 78:cf:7e:2b:bf:18:58:54:23:cc:4b:3d:7e:f4:63:79 u4\@example.org laptop/u4\@example.org\@home 78:cf:7e:2b:bf:18:58:54:23:cc:4b:3d:7e:f4:63:79 u4\@example.org u4\@example.org 78:cf:7e:2b:bf:18:58:54:23:cc:4b:3d:7e:f4:63:79 u4\@example.org u4\@example.org\@home 8c:a6:c0:a5:71:85:0b:89:d3:08:97:22:ae:95:e1:bb u4\@example.org zzz/guests/u2/u4\@example.org 78:cf:7e:2b:bf:18:58:54:23:cc:4b:3d:7e:f4:63:79 u4\@example.org zzz/guests/u2/u4\@example.org\@foo 8c:a6:c0:a5:71:85:0b:89:d3:08:97:22:ae:95:e1:bb u4\@example.org zzz/guests/u3/u4\@example.org fc:0f:eb:52:7a:d2:35:da:89:96:f5:15:0e:85:46:e7 u6\@example.org zzz/guests/u3/u6\@example.org \n\n"; # Now, u2 has two keys in his directory, but u2 can manage only one of # them, since the one added by the admin has two @ in it. Thus the key # added by admin is invisible to u2. try "ssh u2 ukm; ok"; cmp "Hello u2, you manage the following keys: fingerprint userid keyid 8c:a6:c0:a5:71:85:0b:89:d3:08:97:22:ae:95:e1:bb u4\@example.org u4\@example.org \n\n"; # Since admin added key u6@example.org to the directory of u2, u2 is # also able to see it and, in fact, to manage it. try "ssh u3 ukm; ok"; cmp "Hello u3, you manage the following keys: fingerprint userid keyid 8c:a6:c0:a5:71:85:0b:89:d3:08:97:22:ae:95:e1:bb u4\@example.org u4\@example.org fc:0f:eb:52:7a:d2:35:da:89:96:f5:15:0e:85:46:e7 u6\@example.org u6\@example.org \n\n"; ################################################################### # Deletion of keys. try " DEF DEL = ssh %1 ukm del %2 DEF DELOK = DEL %1 %2; ok DEF DELNOK = DEL %1 %2; !ok DEF DELNOMGR = DELNOK %1 %2; /FATAL: You are not managing the key / "; # Deletion requires a keyid. try "ssh u3 ukm del; !ok; /FATAL: keyid required/"; # u3 can, of course, not remove any unmanaged key. try "DELNOMGR u3 u2"; # But u3 can delete u4@example.org and u6@example.org. This will, of course, # not remove the key u4@example.org that u2 manages. try " DELOK u3 u4\@example.org DELOK u3 u6\@example.org "; # After having deleted u4@example.org, u3 cannot remove it again, # even though, u2 still manages that key. try "DELNOMGR u3 u4\@example.org"; # Of course a super-key-manager can remove any (existing) key. try " DELOK admin zzz/guests/u2/u4\@example.org DELNOK admin zzz/guests/u2/u4\@example.org /FATAL: You are not managing the key zzz/guests/u2/u4\@example.org./ DELNOK admin zzz/guests/u2/u4\@example.org\@x /FATAL: You are not managing the key zzz/guests/u2/u4\@example.org./ DELOK admin zzz/guests/u2/u4\@example.org\@foo "; # As the admin could do that via pushing to the gitolite-admin manually, # it's also allowed to delete even non-guest keys. try "DELOK admin u3"; # Let's clean the environment again. try " DELOK admin laptop/u4\@example.org\@home DELOK admin laptop/u4\@example.org DELOK admin u4\@example.org\@home DELOK admin u4\@example.org ADDOK u3 admin u3 "; # Currently the admin has just one key. It cannot be removed. # But after adding another key, deletion should work fine. try " DELNOK admin admin; /FATAL: You cannot delete your last key./ ADDOK u6 admin second/admin; /Adding new public key for admin./ DELOK admin admin DELNOK u6 admin; /FATAL: You are not managing the key admin./ DELNOK u6 second/admin; /FATAL: You cannot delete your last key./ ADDOK admin u6 admin; /Adding new public key for admin./ DELOK u6 second/admin "; ################################################################### # Selfkey management. # If self key management is not switched on in the .gitolite.rc file, # it's not allowed at all. try "ssh u2 ukm add \@second; !ok; /FATAL: selfkey management is not enabled/"; # Let's enable it. system("sed -i \"/'UKM_CONFIG'=>/s/=>{/=>{'SELFKEY_MANAGEMENT'=>1,/\" $h/.gitolite.rc"); # And add self-key-managers to gitolite.conf # chdir("../gitolite-admin") or die "in `pwd`, could not cd ../g-a"; try "glt pull admin origin master; ok"; put "|cut -c5- > conf/gitolite.conf", ' repo gitolite-admin RW+ = admin repo testing RW+ = @all @guest-key-managers = u2 u3 @self-key-managers = u1 u2 @creators = u2 u3 repo pub/CREATOR/..* C = @creators RW+ = CREATOR RW = WRITERS R = READERS '; try " git add conf keydir; ok git commit -m selfkey; ok; /master.* selfkey/ "; try "PUSH admin; ok; gsh; /master -> master/; !/FATAL/" or die text(); # Now we can start with the tests. # Only self key managers are allowed to use selfkey management. # See variable @self-key-managers. try "ssh u3 ukm add \@second; !ok; /FATAL: You are not a selfkey manager./"; # Cannot add keyid that are not alphanumeric. try "ssh u1 ukm add \@second-key; !ok; /FATAL: keyid not allowed:/"; # Add a second key for u1, but leave it pending by not feeding in the # session key. The new user can login, but he/she lives under a quite # random gl_user name and thus is pretty much excluded from everything # except permissions given to @all. If this new id calls ukm without # providing the session key, this (pending) key is automatically # removed from the system. # If a certain keyid is in the system, then it cannot be added again. try " ADDOK u4 u1 \@second ssh admin ukm; ok; /u1 zzz/self/u1/zzz-add-[a-z0-9]{32}-second-u1/ ssh u1 ukm; ok; /u1 \@second .pending add./ ADDNOK u4 u1 \@second; /FATAL: keyid already in use: \@second/ ssh u4 ukm; ok; /pending keyid deleted: \@second/ ssh admin ukm; ok; !/zzz/; !/second/ "; # Not providing a proper ssh public key will abort. Providing a good # ssh public key, which is not a session key makes the key invalid. # The key will, therefore, be deleted by this operation. try " ADDOK u4 u1 \@second echo fake|ssh u4 ukm; !ok; /FATAL: does not seem to be a valid pubkey/ cat $pd/u5.pub | ssh u4 ukm; ok; /session key not accepted/ /pending keyid deleted: \@second/ "; # True addition of a new selfkey is done via piping it to a second ssh # call that uses the new key to call ukm. Note that the first ssh must # have completed its job before the second ssh is able to successfully # log in. This can be done via sleep or via redirecting to a file and # then reading from it. try " # ADDOK u4 u1 \@second | (sleep 2; ssh u4 ukm); ok ADD u4 u1 \@second > session; ok cat session | ssh u4 ukm; ok; /pending keyid added: \@second/ "; # u1 cannot add his/her initial key, since that key can never be # confirmed via ukm, so it is forbidden altogether. In fact, u1 is not # allowed to add any key twice. try " ADDNOK u1 u1 \@first /FATAL: You cannot add a key that already belongs to you./ ADDNOK u4 u1 \@first /FATAL: You cannot add a key that already belongs to you./ "; # u1 also can add more keys, but not under an existing keyid. That can # be done by any of his/her identities (here we choose u4). try " ADDNOK u5 u1 \@second; /FATAL: keyid already in use: \@second/ ADD u5 u4 \@third > session; ok cat session | ssh u5 ukm; ok; /pending keyid added: \@third/ "; # u2 cannot add the same key, but is allowed to use the same name (@third). try " ADDNOK u5 u2 \@third; /FATAL: cannot add key/ /Same key is already available under another userid./ ADD u6 u2 \@third > session; ok cat session | ssh u6 ukm; ok; /pending keyid added: \@third/ "; # u6 can schedule his/her own key for deletion, but cannot actually # remove it. Trying to do so results in bringing back the key. Actual # deletion must be confirmed by another key. try " ssh u6 ukm del \@third; /prepare deletion of key \@third/ ssh u2 ukm; ok; /u2 \@third .pending del./ ssh u6 ukm; ok; /undo pending deletion of keyid \@third/ ssh u6 ukm del \@third; /prepare deletion of key \@third/ ssh u2 ukm del \@third; ok; /pending keyid deleted: \@third/ "; # While in pending-deletion state, it's forbidden to add another key # with the same keyid. It's also forbidden to add a key with the same # fingerprint as the to-be-deleted key). # A new key under another keyid, is OK. try " ssh u1 ukm del \@third; /prepare deletion of key \@third/ ADDNOK u4 u1 \@third; /FATAL: keyid already in use: \@third/ ADDNOK u5 u1 \@fourth; /FATAL: You cannot add a key that already belongs to you./ ADD u6 u1 \@fourth > session; ok ssh u1 ukm; ok; /u1 \@second/ /u1 \@fourth .pending add./ /u1 \@third .pending del./ "; # We can remove a pending-for-addition key (@fourth) by logging in # with a non-pending key. Trying to do anything with key u5 (@third) # will just bring it back to its normal state, but not change the # state of any other key. As already shown above, using u6 (@fourth) # without a proper session key, would remove it from the system. # Here we want to demonstrate that key u1 can delete u6 immediately. try "ssh u1 ukm del \@fourth; /pending keyid deleted: \@fourth/"; # The pending-for-deletion key @third can also be removed via the u4 # (@second) key. try "ssh u4 ukm del \@third; ok; /pending keyid deleted: \@third/"; # Non-existing selfkeys cannot be deleted. try "ssh u4 ukm del \@x; !ok; /FATAL: You are not managing the key \@x./"; gitolite3-3.6.1/contrib/utils/000077500000000000000000000000001241446647300162375ustar00rootroot00000000000000gitolite3-3.6.1/contrib/utils/ipa_groups.pl000077500000000000000000000172421241446647300207550ustar00rootroot00000000000000#!/usr/bin/env perl # # ipa_groups.pl # # See perldoc for usage # use Net::LDAP; use Net::LDAP::Control::Paged; use Net::LDAP::Constant qw(LDAP_CONTROL_PAGED); use strict; use warnings; my $usage = < 'require', cafile => '/etc/pki/tls/certs/prod.example.net_CA.crt' ); # Base DN to search my $base_dn = 'dc=prod,dc=example,dc=net'; # User for binding to LDAP server with my $user = 'uid=svc_gitolite_bind,cn=sysaccounts,cn=etc,dc=prod,dc=example,dc=net'; my $pass = 'reallysecurepasswordstringhere'; ## Below variables should not need to be changed under normal circumstances # OU where groups are located. Anything return that is not within this OU is # removed from results. This OU is static on FreeIPA so will only need updating # if you want to support other LDAP servers. This is a regex so can be set to # anything you want (E.G '.*'). my $groups_ou = qr/cn=groups,cn=accounts,${base_dn}$/; # strip path - if you want to return the full path of the group object then set # this to 0 my $strip_group_paths = 1; # Number of seconds before timeout (for each query) my $timeout=5; # user object class my $user_oclass = 'person'; # group attribute my $group_attrib = 'memberOf'; ## END OF CONFIG SECTION # Catch timeouts here $SIG{'ALRM'} = sub { die "LDAP queries timed out"; }; alarm($timeout); # try each server until timeout is reached, has very fast failover if a server # is totally unreachable my $ldap = Net::LDAP->new(@ldap_hosts, %ldap_opts) || die "Error connecting to specified servers: $@ \n"; my $mesg = $ldap->bind( dn => $user, password => $pass ); if ($mesg->code()) { die ("error:", $mesg->code(),"\n", "error name: ",$mesg->error_name(),"\n", "error text: ",$mesg->error_text(),"\n"); } # How many LDAP query results to grab for each paged round # Set to under 1000 to limit load on LDAP server my $page = Net::LDAP::Control::Paged->new(size => 500); # @queries is an array or array references. We initially fill it up with one # arrayref (The first LDAP search) and then add more during the execution. # First start by resolving the group. my @queries = [ ( base => $base_dn, filter => "(&(objectClass=${user_oclass})(uid=${uid}))", control => [ $page ], ) ]; # array to store groups matching $groups_ou my @verified_groups; # Loop until @queries is empty... foreach my $queryref (@queries) { # set cookie for paged querying my $cookie; alarm($timeout); while (1) { # Perform search my $mesg = $ldap->search( @{$queryref} ); foreach my $entry ($mesg->entries) { my @groups = $entry->get_value($group_attrib); # find any groups matching $groups_ou regex and push onto $verified_groups array foreach my $group (@groups) { if ($group =~ /$groups_ou/) { push @verified_groups, $group; } } } # Only continue on LDAP_SUCCESS $mesg->code and last; # Get cookie from paged control my($resp) = $mesg->control(LDAP_CONTROL_PAGED) or last; $cookie = $resp->cookie or last; # Set cookie in paged control $page->cookie($cookie); } # END: while(1) # Reset the page control for the next query $page->cookie(undef); if ($cookie) { # We had an abnormal exit, so let the server know we do not want any more $page->cookie($cookie); $page->size(0); $ldap->search( @{$queryref} ); # Then die die("LDAP query unsuccessful"); } } # END: foreach my $queryref (...) # we're assuming that the group object looks something like # cn=name,cn=groups,cn=accounts,dc=X,dc=Y and there are no ',' chars in group # names if ($strip_group_paths) { for (@verified_groups) { s/^cn=([^,]+),.*$/$1/g }; } foreach my $verified (@verified_groups) { print $verified . "\n"; } alarm(0); __END__ =head1 NAME ipa_groups.pl =head2 VERSION 0.1.1 =head2 DESCRIPTION Connects to one or more FreeIPA-based LDAP servers in a first-reachable fashion and returns a newline separated list of groups for a given uid. Uses memberOf attribute and thus supports nested groups. =head2 AUTHOR Richard Clark =head2 FreeIPA vs Generic LDAP This script uses regular LDAP, but is focussed on support for FreeIPA, where users and groups are generally contained within single OUs, and memberOf attributes within the user object are enumerated with a recursive list of groups that the user is a member of. It is mostly impossible to provide generic out of the box LDAP support due to varying schemas, supported extensions and overlays between implementations. =head2 CONFIGURATION =head3 LDAP Bind Account To setup an LDAP bind user in FreeIPA, create a svc_gitolite_bind.ldif file along the following lines: dn: uid=svc_gitolite_bind,cn=sysaccounts,cn=etc,dc=prod,dc=example,dc=net changetype: add objectclass: account objectclass: simplesecurityobject uid: svc_gitolite_bind userPassword: reallysecurepasswordstringhere passwordExpirationTime: 20150201010101Z nsIdleTimeout: 0 Then create the service account user, using ldapmodify authenticating as the the directory manager account (or other acccount with appropriate privileges to the sysaccounts OU): $ ldapmodify -h auth-ldap-001.prod.example.net -Z -x -D "cn=Directory Manager" -W -f svc_gitolite_bind.ldif =head3 Required Configuration The following variables within the C<## CONFIG SECTION ##> need to be configured before the script will work. C<@ldap_hosts> - Should be set to an array of URIs or hosts to connect to. Net::LDAP will attempt to connect to each host in this list and stop on the first reachable server. The example shows TLS-supported URIs, if you want to use plain-text LDAP then set the protocol part of the URI to LDAP:// or just provide hostnames as this is the default behavior for Net::LDAP. C<%ldap_opts> - To use LDAP-over-TLS, provide the CA certificate for your LDAP servers. To use plain-text LDAP, then empty this hash of it's values or provide other valid arguments to Net::LDAP. C<%base_dn> - This can either be set to the 'true' base DN for your directory, or alternatively you can set it the the OU that your users are located in (E.G cn=users,cn=accounts,dc=prod,dc=example,dc=net). C<$user> - Provide the full Distinguished Name of your directory bind account as configured above. C<$pass> - Set to password of your directory bind account as configured above. =head3 Optional Configuration C<$groups_ou> - By default this is a regular expression matching the default groups OU. Any groups not matching this regular expression are removed from the search results. This is because FreeIPA enumerates non-user type groups (E.G system, sudoers, policy and other types) within the memberOf attribute. To change this behavior, set C<$groups_ou> to a regex matching anything you want (E.G: '.*'). C<$strip_group_paths> - If this is set to perl boolean false (E.G '0') then groups will be returned in DN format. Default is true, so just the short/CN value is returned. C<$timeout> - Number of seconds to wait for an LDAP query before determining that it has failed and trying the next server in the list. This does not affect unreachable servers, which are failed immediately. C<$user_oclass> - Object class of the user to search for. C<$group_attrib> - Attribute to search for within the user object that denotes the membership of a group. =cut gitolite3-3.6.1/contrib/utils/ldap_groups.sh000077500000000000000000000010631241446647300211150ustar00rootroot00000000000000#!/bin/bash # author: damien.nozay@gmail.com # Given a username, # Provides a space-separated list of groups that the user is a member of. # # see http://gitolite.com/gitolite/auth.html#ldap # GROUPLIST_PGM => /path/to/ldap_groups.sh ldap_groups() { username=$1; # this relies on openldap / pam_ldap to be configured properly on your # system. my system allows anonymous search. echo $( ldapsearch -x -LLL "(&(objectClass=posixGroup)(memberUid=${username}))" cn \ | grep "^cn" \ | cut -d' ' -f2 ); } ldap_groups $@ gitolite3-3.6.1/contrib/utils/rc-format-v3.4000077500000000000000000000140531241446647300205520ustar00rootroot00000000000000#!/usr/bin/perl # help with rc file format change at v3.4 -- help upgrade v3 rc files from # v3.3 and below to the new v3.4 and above format # once you upgrade gitolite past 3.4, you may want to use the new rc file # format, because it's really much nicer (just to recap: the old format will # still work, in fact internally the new format gets converted to the old # format before actually being used. However, the new format makes it much # easier to enable and disable features). # PLEASE SEE WARNINGS BELOW # this program helps you upgrade your rc file. # STEPS # cd gitolite-source-repo-clone # contrib/utils/upgrade-rc33 /path/to/old.gitolite.rc > new.gitolite.rc # WARNINGS # make sure you also READ ALL ERROR/WARNING MESSAGES GENERATED # make sure you EXAMINE THE FILE AND CHECK THAT EVERYTHING LOOKS GOOD before using it # be especially careful about # variables which contains single/double quotes or other special characters # variables that stretch across multiple lines # features which take arguments (like 'renice') # new features you've enabled which don't exist in the default rc # ---------------------------------------------------------------------- use strict; use warnings; use 5.10.0; use Cwd; use Data::Dumper; $Data::Dumper::Terse = 1; $Data::Dumper::Indent = 1; $Data::Dumper::Sortkeys = 1; BEGIN { $ENV{HOME} = getcwd; $ENV{HOME} .= "/.home.rcupgrade.$$"; mkdir $ENV{HOME} or die "mkdir '$ENV{HOME}': $!\n"; } END { system("rm -rf ./.home.rcupgrade.$$"); } use lib "./src/lib"; use Gitolite::Rc; { no warnings 'redefine'; sub Gitolite::Common::gl_log { } } # ---------------------------------------------------------------------- # everything happens inside a fresh v3.6.1+ gitolite clone; no other # directories are used. # the old rc file to be migrated is somewhere *else* and is supplied as a # command line argument. # ---------------------------------------------------------------------- my $oldrc = shift or die "need old rc filename as arg-1\n"; { package rcup; do $oldrc; } my %oldrc; { no warnings 'once'; %oldrc = %rcup::RC; } delete $rcup::{RC}; { my @extra = sort keys %rcup::; warn "**** WARNING ****\nyou have variables declared outside the %RC hash; you must handle them manually\n" if @extra; } # this is the new rc text being built up my $newrc = glrc('default-text'); # ---------------------------------------------------------------------- # default disable all features in newrc map { disable( $_, 'sq' ) } (qw(help desc info perms writable ssh-authkeys git-config daemon gitweb)); # map { disable($_, '') } (qw(GIT_CONFIG_KEYS)); set_s('HOSTNAME'); set_s( 'UMASK', 'num' ); set_s( 'GIT_CONFIG_KEYS', 'sq' ); set_s( 'LOG_EXTRA', 'num' ); set_s( 'DISPLAY_CPU_TIME', 'num' ); set_s( 'CPU_TIME_WARN_LIMIT', 'num' ); set_s('SITE_INFO'); set_s('LOCAL_CODE'); if ( $oldrc{WRITER_CAN_UPDATE_DESC} ) { die "tell Sitaram he changed the default rc too much" unless $newrc =~ /rc variables used by various features$/m; $newrc =~ s/(rc variables used by various features\n)/$1\n # backward compat\n WRITER_CAN_UPDATE_DESC => 1,\n/; delete $oldrc{WRITER_CAN_UPDATE_DESC}; } if ( $oldrc{ROLES} ) { my $t = ''; for my $r ( sort keys %{ $oldrc{ROLES} } ) { $t .= ( " " x 8 ) . $r . ( " " x ( 28 - length($r) ) ) . "=> 1,\n"; } $newrc =~ s/(ROLES *=> *\{\n).*?\n( *\},)/$1$t$2/s; delete $oldrc{ROLES}; } if ( $oldrc{DEFAULT_ROLE_PERMS} ) { warn "DEFAULT_ROLE_PERMS has been replaced by per repo option\nsee http://gitolite.com/gitolite/wild.html\n"; delete $oldrc{DEFAULT_ROLE_PERMS}; } # the following is a bit like the reverse of what the new Rc.pm does... for my $l ( split /\n/, $Gitolite::Rc::non_core ) { next if $l =~ /^ *#/ or $l !~ /\S/; my ( $name, $where, $module ) = split ' ', $l; $module = $name if $module eq '.'; ( $module = $name ) .= "::" . lc($where) if $module eq '::'; # if you find $module as an element of $where, enable $name enable($name) if miw( $module, $where ); } # now deal with commands if ( $oldrc{COMMANDS} ) { for my $c ( sort keys %{ $oldrc{COMMANDS} } ) { if ( $oldrc{COMMANDS}{$c} == 1 ) { enable($c); # we don't handle anything else right (and so far only git-annex # is affected, as far as I remember) delete $oldrc{COMMANDS}{$c}; } } } print $newrc; for my $w (qw(INPUT POST_COMPILE PRE_CREATE ACCESS_1 POST_GIT PRE_GIT ACCESS_2 POST_CREATE SYNTACTIC_SUGAR)) { delete $oldrc{$w} unless scalar( @{ $oldrc{$w} } ); } delete $oldrc{COMMANDS} unless scalar keys %{ $oldrc{COMMANDS} }; exit 0 unless %oldrc; warn "the following parts of the old rc were NOT converted:\n"; print STDERR Dumper \%oldrc; # ---------------------------------------------------------------------- # set scalars that the new file defaults to "commented out" sub set_s { my ( $key, $type ) = @_; $type ||= ''; return unless exists $oldrc{$key}; # special treatment for UMASK $oldrc{$key} = substr( "00" . sprintf( "%o", $oldrc{$key} ), -4 ) if ( $key eq 'UMASK' ); $newrc =~ s/# $key /$key /; # uncomment if needed if ( $type eq 'num' ) { $newrc =~ s/$key ( *=> *).*/$key $1$oldrc{$key},/; } elsif ( $type eq 'sq' ) { $newrc =~ s/$key ( *=> *).*/$key $1'$oldrc{$key}',/; } else { $newrc =~ s/$key ( *=> *).*/$key $1"$oldrc{$key}",/; } delete $oldrc{$key}; } sub disable { my ( $key, $type ) = @_; if ( $type eq 'sq' ) { $newrc =~ s/^( *)'$key'/$1# '$key'/m; } else { $newrc =~ s/^( *)$key\b/$1# $key/m; } } sub enable { my $key = shift; $newrc =~ s/^( *)# *'$key'/$1'$key'/m; return if $newrc =~ /^ *'$key'/m; $newrc =~ s/(add new commands here.*\n)/$1 '$key',\n/; } sub miw { my ( $m, $w ) = @_; return 0 unless $oldrc{$w}; my @in = @{ $oldrc{$w} }; my @out = grep { !/^$m$/ } @{ $oldrc{$w} }; $oldrc{$w} = \@out; return not scalar(@in) == scalar(@out); } gitolite3-3.6.1/convert-gitosis-conf000077500000000000000000000071231241446647300174520ustar00rootroot00000000000000#!/usr/bin/perl -w # # migrate gitosis.conf to gitolite.conf format # # Based on gl-conf-convert by: Sitaram Chamarty # Rewritten by: Behan Webster # use strict; use warnings; if (not @ARGV and -t or @ARGV and $ARGV[0] eq '-h') { print "Usage:\n gl-conf-convert < gitosis.conf > gitolite.conf\n(please see the documentation for details)\n"; exit 1; } my @comments = (); my $groupname; my %groups; my $reponame; my %repos; while (<>) { # not supported if (/^repositories *=/ or /^map /) { print STDERR "not supported: $_"; s/^/NOT SUPPORTED: /; print; next; } # normalise whitespace to help later regexes chomp; s/\s+/ /g; s/ ?= ?/ = /; s/^ //; s/ $//; if (/^\s*$/ and @comments > 1) { @{$repos{$reponame}{comments}} = @comments if $reponame; @{$groups{$groupname}{comments}} = @comments if $groupname; @comments = (); } elsif (/^\s*#/) { push @comments, $_; } elsif (/^\[repo\s+(.*?)\]$/) { $groupname = ''; $reponame = $1; $reponame =~ s/\.git$//; } elsif (/^\[gitosis\]$/) { $groupname = ''; $reponame = '@all'; } elsif (/^gitweb\s*=\s*yes/i) { push @{$repos{$reponame}{R}}, 'gitweb'; } elsif (/^daemon\s*=\s*yes/i) { push @{$repos{$reponame}{R}}, 'daemon'; } elsif (/^description\s*=\s*(.+?)$/) { $repos{$reponame}{desc} = $1; } elsif (/^owner\s*=\s*(.+?)$/) { $repos{$reponame}{owner} = $1; } elsif (/^\[group\s+(.*)\]$/) { $reponame = ''; $groupname = $1; } elsif (/^members\s*=\s*(.*)/) { push @{$groups{$groupname}{users}}, map {s/\@([^.]+)$/_$1/g; $_} split(' ', $1); } elsif (/^write?able\s*=\s*(.*)/) { foreach my $repo (split(' ', $1)) { $repo =~ s/\.git$//; push @{$repos{$repo}{RW}}, "\@$groupname"; } } elsif (/^readonly\s*=\s*(.*)/) { foreach my $repo (split(' ', $1)) { $repo =~ s/\.git$//; push @{$repos{$repo}{R}}, "\@$groupname"; } } } #use Data::Dumper; #print Dumper(\%repos); #print Dumper(\%groups); # Groups print "#\n# Groups\n#\n\n"; foreach my $grp (sort keys %groups) { next unless @{$groups{$grp}{users}}; printf join("\n", @{$groups{$grp}{comments}})."\n" if $groups{$grp}{comments}; printf "\@%-19s = %s\n", $grp, join(' ', @{$groups{$grp}{users}}); } # Gitweb print "\n#\n# Gitweb\n#\n\n"; foreach my $repo (sort keys %repos) { if ($repos{$repo}{desc}) { @{$repos{$repo}{R}} = grep(!/^gitweb$/, @{$repos{$repo}{R}}); print $repo; print " \"$repos{$repo}{owner}\"" if $repos{$repo}{owner}; print " = \"$repos{$repo}{desc}\"\n"; } } # Repos print "\n#\n# Repos\n#\n"; foreach my $repo (sort keys %repos) { print "\n"; printf join("\n", @{$repos{$repo}{comments}})."\n" if $repos{$repo}{comments}; #if ($repos{$repo}{desc}) { # @{$repos{$repo}{R}} = grep(!/^gitweb$/, @{$repos{$repo}{R}}); #} print "repo\t$repo\n"; foreach my $access (qw(RW+ RW R)) { next unless $repos{$repo}{$access}; my @keys; foreach my $key (@{$repos{$repo}{$access}}) { if ($key =~ /^\@(.*)/) { next unless defined $groups{$1} and @{$groups{$1}{users}}; } push @keys, $key; } printf "\t$access\t= %s\n", join(' ', @keys) if @keys; } #if ($repos{$repo}{desc}) { # print $repo; # print " \"$repos{$repo}{owner}\"" if $repos{$repo}{owner}; # print " = \"$repos{$repo}{desc}\"\n"; #} } gitolite3-3.6.1/install000077500000000000000000000043561241446647300150430ustar00rootroot00000000000000#!/usr/bin/perl use strict; use warnings; # Clearly you don't need a program to make one measly symlink, but the git # describe command involved in generating the VERSION string is a bit fiddly. use Getopt::Long; use FindBin; # meant to be run from the root of the gitolite tree, one level above 'src' BEGIN { $ENV{GL_BINDIR} = $FindBin::RealBin . "/src"; } BEGIN { $ENV{GL_LIBDIR} = "$ENV{GL_BINDIR}/lib"; } use lib $ENV{GL_LIBDIR}; use Gitolite::Common; =for usage Usage (from gitolite clone directory): ./install to run gitolite using an absolute or relative path, for example 'src/gitolite' or '/full/path/to/this/dir/src/gitolite' ./install -ln [] to symlink just the gitolite executable to some that is in $PATH. defaults to $HOME/bin if not specified. is assumed to exist; gitolite will not create it. Please provide a full path, not a relative path. ./install -to to copy the entire 'src' directory to . If is not in $PATH, use the full path to run gitolite commands. Please provide a full path, not a relative path. Simplest use, if $HOME/bin exists and is in $PATH, is: git clone git://github.com/sitaramc/gitolite gitolite/install -ln # now run setup gitolite setup -pk /path/to/YourName.pub =cut my ( $to, $ln, $help, $quiet ); GetOptions( 'to=s' => \$to, 'ln:s' => \$ln, 'help|h' => \$help, 'quiet|q' => \$quiet, ); usage() if $to and $ln or $help; $ln = "$ENV{HOME}/bin" if defined($ln) and not $ln; for my $d ($ln, $to) { if ($d and not -d $d) { print STDERR "FATAL: '$d' does not exist.\n"; usage(); } } chdir($ENV{GL_BINDIR}); my $version = `git describe --tags --long --dirty=-dt 2>/dev/null`; unless ($version =~ /^v\d/) { print STDERR "git describe failed; cannot deduce version number\n"; $version = "(unknown)"; } if ($to) { _mkdir($to); system("cp -RpP * $to"); _print( "$to/VERSION", $version ); } elsif ($ln) { ln_sf( $ENV{GL_BINDIR}, "gitolite", $ln ); _print( "VERSION", $version ); } else { say "use the following full path for gitolite:"; say "\t$ENV{GL_BINDIR}/gitolite"; _print( "VERSION", $version ); } gitolite3-3.6.1/src/000077500000000000000000000000001241446647300142265ustar00rootroot00000000000000gitolite3-3.6.1/src/VREF/000077500000000000000000000000001241446647300147705ustar00rootroot00000000000000gitolite3-3.6.1/src/VREF/COUNT000077500000000000000000000031271241446647300156110ustar00rootroot00000000000000#!/bin/sh # gitolite VREF to count number of changed/new files in a push # see gitolite docs for what the first 7 arguments mean # inputs: # arg-8 is a number # arg-9 is optional, and can be "NEWFILES" # outputs (STDOUT) # arg-7 if the number of changed (or new, if arg-9 supplied) files is > arg-8 # otherwise nothing # exit status: # always 0 die() { echo "$@" >&2; exit 1; } [ -z "$8" ] && die "not meant to be run manually" newsha=$3 oldtree=$4 newtree=$5 refex=$7 max=$8 nf= [ "$9" = "NEWFILES" ] && nf='--diff-filter=A' # NO_SIGNOFF implies NEWFILES [ "$9" = "NO_SIGNOFF" ] && nf='--diff-filter=A' # count files against all the other commits in the system not just $oldsha # (why? consider what is $oldtree when you create a new branch, or what is # $oldsha when you update an old feature branch from master and then push it count=`git log --name-only $nf --format=%n $newtree --not --all | grep . | sort -u | perl -ne '}{print "$."'` [ $count -gt $max ] && { # count has been exceeded. If $9 was NO_SIGNOFF there's still a chance # for redemption -- if the top commit has a proper signed-off by line [ "$9" = "NO_SIGNOFF" ] && { author_email=$(git log --format=%ae -1 $newsha) git cat-file -p $newsha | egrep -i >/dev/null "^ *$count +new +files +signed-off by: *$author_email *$" && exit 0 echo $refex top commit message should include the text \'$count new files signed-off by: $author_email\' exit 0 } echo -n $refex "(too many " [ -n "$nf" ] && echo -n "new " || echo -n "changed " echo "files in this push)" } exit 0 gitolite3-3.6.1/src/VREF/EMAIL-CHECK000077500000000000000000000051441241446647300163640ustar00rootroot00000000000000#!/usr/bin/perl # gitolite VREF to check if all *new* commits have author == pusher # THIS IS NOT READY TO USE AS IS # ------------------------------ # you MUST change the 'email_ok()' sub to suit *YOUR* site's # gitolite username -> author email mapping! # See bottom of the program for important philosophical notes. use strict; use warnings; # mapping between gitolite userid and correct email address is encapsulated in # this subroutine; change as you like sub email_ok { my ($author_email) = shift; my $expected_email = "$ENV{GL_USER}\@atc.tcs.com"; return $author_email eq $expected_email; } my ( $ref, $old, $new ) = @ARGV; for my $rev (`git log --format="%ae\t%h\t%s" $new --not --all`) { chomp($rev); my ( $author_email, $hash, $subject ) = split /\t/, $rev; # again, we use the trick that a vref can just choose to die instead of # passing back a vref, having it checked, etc., if it's more convenient die "$ENV{GL_USER}, you can't push $hash authored by $author_email\n" . "\t(subject of commit was $subject)\n" unless email_ok($author_email); } exit 0; __END__ The following discussion is for people who want to enforce this check on ALL their developers (i.e., not just the newbies). Doing this breaks the "D" in "DVCS", forcing all your developers to work to a centralised model as far as pushes are concerned. It prevents amending someone else's commit and pushing (this includes rebasing, cherry-picking, and so on, which are all impossible now). It also makes *any* off-line collabaration between two developers useless, because neither of them can push the result to the server. PHBs should note that validating the committer ID is NOT the same as reviewing the code and running QA/tests on it. If you're not reviewing/QA-ing the code, it's probably worthless anyway. Conversely, if you *are* going to review the code and run QA/tests anyway, then you don't really need to validate the author email! In a DVCS, if you *pushed* a series of commits, you have -- in some sense -- signed off on them. The most formal way to "sign" a series is to tack on and push a gpg-signed tag, although most people don't go that far. Gitolite's log files are designed to preserve that accountability to *some* extent, though; see contrib/adc/who-pushed for an admin defined command that quickly and easily tells you who *pushed* a particular commit. Anyway, the point is that the only purpose of this script is to * pander to someone who still has not grokked *D*VCS OR * tick off an item in some stupid PHB's checklist gitolite3-3.6.1/src/VREF/FILETYPE000077500000000000000000000021131241446647300161340ustar00rootroot00000000000000#!/bin/sh # gitolite VREF to find autogenerated files # *completely* site specific; use it as an illustration of what can be done # with gitolite VREFs if you wish # see gitolite docs for what the first 7 arguments mean # inputs: # arg-8 is currently only one possible value: AUTOGENERATED # outputs (STDOUT) # arg-7 if any files changed in the push look like they were autogenerated # otherwise nothing # exit status: # always 0 die() { echo "$@" >&2; exit 1; } [ -z "$8" ] && die "not meant to be run manually" newsha=$3 oldtree=$4 newtree=$5 refex=$7 option=$8 [ "$option" = "AUTOGENERATED" ] && { # currently we only look for ".java" programs with the string "Generated # by the protocol buffer compiler. DO NOT EDIT" in them. git log --name-only $nf --format=%n $newtree --not --all | grep . | sort -u | grep '\.java$' | while read fn do git show "$newtree:$fn" | egrep >/dev/null \ 'Generated by the protocol buffer compiler. +DO NOT EDIT' || continue echo $refex exit 0 done } gitolite3-3.6.1/src/VREF/MAX_NEWBIN_SIZE000077500000000000000000000020411241446647300172340ustar00rootroot00000000000000#!/usr/bin/perl use strict; use warnings; # gitolite VREF to check max size of new binary files # see gitolite docs for what the first 7 arguments mean # inputs: # arg-8 is a number # outputs (STDOUT) # arg-7 if any new binary files exist that are greater in size than arg-8 # *and* there is no "signed-off by" line for such a file in the top commit # message. # # Otherwise nothing # exit status: # always 0 die "not meant to be run manually" unless $ARGV[7]; my ( $newsha, $oldtree, $newtree, $refex, $max ) = @ARGV[ 2, 3, 4, 6, 7 ]; # / (.*) +\| Bin 0 -> (\d+) bytes/ chomp( my $author_email = `git log --format=%ae -1 $newsha` ); my $msg = `git cat-file -p $newsha`; $msg =~ s/\t/ /g; # makes our regexes simpler for my $newbin (`git diff --stat=999,999 $oldtree $newtree | grep Bin.0.-`) { next unless $newbin =~ /^ (.*) +\| +Bin 0 -> (\d+) bytes/; my ( $f, $s ) = ( $1, $2 ); next if $s <= $max; next if $msg =~ /^ *$f +signed-off by: *$author_email *$/mi; print "$refex $f is larger than $max"; } exit 0 gitolite3-3.6.1/src/VREF/MERGE-CHECK000066400000000000000000000032531241446647300163700ustar00rootroot00000000000000#!/usr/bin/perl use strict; use warnings; # gitolite VREF to check if there are any merge commits in the current push. # THIS IS DEMO CODE; please read all comments below as well as # doc/vref.mkd before trying to use this. # usage in conf/gitolite.conf goes like this: # - VREF/MERGE_CHECK/master = @all # # reject only if the merge commit is being pushed to the master branch # - VREF/MERGE_CHECK = @all # # reject merge commits to any branch my $ref = $ARGV[0]; my $oldsha = $ARGV[1]; my $newsha = $ARGV[2]; my $refex = $ARGV[6]; # The following code duplicates some code from parse_conf_line() and some from # check_ref(). This duplication is the only thing that is preventing me from # removing the "M" permission code from 'core' gitolite and using this # instead. However, it does demonstrate how you would do this if you had to # create any other similar features, for example someone wanted "no non-merge # first-parent", which is far too specific for me to add to 'core'. # -- begin duplication -- my $branch_refex = $ARGV[7] || ''; if ($branch_refex) { $branch_refex =~ m(^refs/) or $branch_refex =~ s(^)(refs/heads/); } else { $branch_refex = 'refs/.*'; } exit 0 unless $ref =~ /^$branch_refex/; # -- end duplication -- # we can't run this check for tag creation or new branch creation, because # 'git log' does not deal well with $oldsha = '0' x 40. if ( $oldsha eq "0" x 40 or $newsha eq "0" x 40 ) { print STDERR "ref create/delete ignored for purposes of merge-check\n"; exit 0; } my $ret = `git rev-list -n 1 --merges $oldsha..$newsha`; print "$refex FATAL: merge commits not allowed\n" if $ret =~ /./; exit 0; gitolite3-3.6.1/src/VREF/VOTES000077500000000000000000000056471241446647300156320ustar00rootroot00000000000000#!/bin/sh # gitolite VREF to count votes before allowing pushes to certain branches. # This approximates gerrit's voting (but it is SHA based; I believe Gerrit is # more "changeset" based). Here's how it works: # - A normal developer "bob" proposes changes to master by pushing a commit to # "pers/bob/master", then informs the voting members by email. # - Some or all of the voting members fetch and examine the commit. If they # approve, they "vote" for the commit like so. For example, say voting # member "alice" fetched bob's proposed commit into "bob-master" on her # clone, then tested or reviewed it. She would approve it by running: # git push origin bob-master:votes/alice/master # - Once enough votes have been tallied (hopefully there is normal team # communication that says "hey I approved your commit", or it can be checked # by 'git ls-remote origin' anyway), Bob, or any developer, can push the # same commit (same SHA) to master and the push will succeed. # - Finally, a "trusted" developer can push a commit to master without # worrying about the voting restriction at all. # The config for this example would look like this: # repo foo # # allow personal branches (to submit proposed changes) # RW+ pers/USER/ = @devs # - pers/ = @all # # # allow only voters to vote # RW+ votes/USER/ = @voters # - votes/ = @all # # # normal access rules go here; should allow *someone* to push master # RW+ = @devs # # # 2 votes required to push master, but trusted devs don't have this restriction # RW+ VREF/VOTES/2/master = @trusted-devs # - VREF/VOTES/2/master = @devs # Note: "2 votes required to push master" means at least 2 refs matching # "votes/*/master" have the same SHA as the one currently being pushed. # ---------------------------------------------------------------------- # see gitolite docs for what the first 7 arguments mean # inputs: # arg-8 is a number; see below # arg-9 is a simple branch name (i.e., "master", etc). Currently this code # does NOT do vote counting for branch names with more than one component # (like foo/bar). # outputs (STDOUT) # nothing # exit status: # always 0 die() { echo "$@" >&2; exit 1; } [ -z "$8" ] && die "not meant to be run manually" ref=$1 newsha=$3 refex=$7 votes_needed=$8 branch=$9 # nothing to do if the branch being pushed is not "master" (using our example) [ "$ref" = "refs/heads/$branch" ] || exit 0 # find how many votes have come in votes=`git for-each-ref refs/heads/votes/*/$branch | grep -c $newsha` # send back a vref if we don't have the minimum votes needed. For trusted # developers this will invoke the RW+ rule and pass anyway, but for others it # will invoke the "-" rule and fail. [ $votes -ge $votes_needed ] || echo $refex "require at least $votes_needed votes to push $branch" exit 0 gitolite3-3.6.1/src/VREF/lock000077500000000000000000000016011241446647300156440ustar00rootroot00000000000000#!/usr/bin/perl use strict; use warnings; use lib $ENV{GL_LIBDIR}; use Gitolite::Common; # gitolite VREF to lock and unlock (binary) files. Requires companion command # 'lock' to be enabled; see doc/locking.mkd for details. # ---------------------------------------------------------------------- # see gitolite docs for what the first 7 arguments mean die "not meant to be run manually" unless $ARGV[6]; my $ff = "$ENV{GL_REPO_BASE}/$ENV{GL_REPO}.git/gl-locks"; exit 0 unless -f $ff; our %locks; my $t = slurp($ff); eval $t; _die "do '$ff' failed with '$@', contact your administrator" if $@; my ( $oldtree, $newtree, $refex ) = @ARGV[ 3, 4, 6 ]; for my $file (`git diff --name-only $oldtree $newtree`) { chomp($file); if ( $locks{$file} and $locks{$file}{USER} ne $ENV{GL_USER} ) { print "$refex '$file' locked by '$locks{$file}{USER}'"; last; } } exit 0 gitolite3-3.6.1/src/VREF/partial-copy000077500000000000000000000021471241446647300173260ustar00rootroot00000000000000#!/bin/sh # push updated branches back to the "main" repo. # This must be run as the *last* VREF, though it doesn't matter what # permission you give to it die() { echo "$@" >&2; exit 1; } repo=$GL_REPO user=$GL_USER ref=$1 # we're running like an update hook old=$2 new=$3 # never send any STDOUT back, to avoid looking like a ref. If we fail, git # will catch it by our exit code exec >&2 main=`git config --file $GL_REPO_BASE/$repo.git/config --get gitolite.partialCopyOf`; [ -z "$main" ] && exit 0 rand=$$ export GL_BYPASS_ACCESS_CHECKS=1 if [ "$new" = "0000000000000000000000000000000000000000" ] then # special case for deleting a ref (this is why it is important to put this # VREF as the last one; if we got this far he is allowed to delete it) git push -f $GL_REPO_BASE/$main.git :$ref || die "FATAL: failed to delete $ref" exit 0 fi git push -f $GL_REPO_BASE/$main.git $new:refs/partial/br-$rand || die "FATAL: failed to send $new" cd $GL_REPO_BASE/$main.git git update-ref -d refs/partial/br-$rand git update-ref $ref $new $old || die "FATAL: update-ref for $ref failed" exit 0 gitolite3-3.6.1/src/VREF/refex-expr000077500000000000000000000076271241446647300170170ustar00rootroot00000000000000#!/usr/bin/perl use strict; use warnings; # see bottom of this file for instructons and IMPORTANT WARNINGS! # ---------------------------------------------------------------------- my $rule = $ARGV[7]; die "\n\nFATAL: GL_REFEX_EXPR_ doesn't exist\n(your admin probably forgot the rc file change needed for this to work)\n\n" unless exists $ENV{ "GL_REFEX_EXPR_" . $rule }; my $res = $ENV{ "GL_REFEX_EXPR_" . $rule } || 0; print "$ARGV[6] ($res)\n" if $res; exit 0; __END__ ------------------------------------------------------------------------ IMPORTANT WARNINGS: * has not been tested heavily * SO PLEASE TEST YOUR SPECIFIC USE CASE THOROUGHLY! * read the NOTES section below * syntax and semantics are to be considered beta and may change as I find better use cases ------------------------------------------------------------------------ Refex expressions, like VREFs, are best used as additional "deny" rules, to deny combinations that the normal ruleset cannot detect. To enable this, uncomment 'refex-expr' in the ENABLE list in the rc file. It allows you to say things like "don't allow users u3 and u4 to change the Makefile in the master branch" (i.e., they can change any other file in master, or the Makefile in any other branch, but not that specific combo). repo foo RW+ = u1 u2 # line 1 RW+ master = u3 u4 # line 2 RW+ = u3 u4 # line 3 RW+ VREF/NAME/Makefile = u3 u4 # line 4 - master and VREF/NAME/Makefile = u3 u4 # line 5 Line 5 is a "refex expression". Here are the rules: * for each refex in the expression ("master" and "VREF/NAME/Makefile" in this example), a count is kept of the number of times the EXACT refex was matched and allowed in the *normal* rules (here, lines 2 and 4) during this push. * the expression is evaluated based on these counts. 0 is false, and any non-zero is true (see more examples later). The truth value of the expression determines whether the refex expression matched. You can use any logical or arithmetic expression using refexes as operands and using these operators: not and or xor + - == -lt -gt -eq -le -ge -ne Parens are not allowed. Precedence is as you might expect for those operators. It's actually perl that is evaluating it (you can guess what the '-lt' etc., get translated to) so if in doubt, check 'man perlop'. * the refexes that form the terms of the expression (in this case, lines 2 and 4) MUST come before the expression itself (i.e., line 5). * note the words "EXACT refex was matched" above. Let's say you add "u3" to line 1. Then the refex expression in line 5 would never match for u3. This is because line 1 prevents line 2 from matching (being more general *and* appearing earlier), so the count for the "master" refex would be 0. If "master" is 0 (false), then "master and " is also false. (Same thing is you swap lines 2 and 3; i.e., put the "RW+ = ..." before the "RW+ master = ..."). Put another way, the terms in the refex expression are refexes, not refs. Merely pushing the master branch does not mean the count for "master" increases; it has to *match* on a line that has "master" as the refex. Here are some more examples: * user u2 is allowed to push either 'doc/' or 'src/' but not both repo foo RW+ = u1 u2 u3 RW+ VREF/NAME/doc/ = u2 RW+ VREF/NAME/src/ = u2 - VREF/NAME/doc/ and VREF/NAME/src/ = u2 * user u3 is allowed to push at most 2 files to conf/ repo foo RW+ = u1 u2 u3 RW+ VREF/NAME/conf/ = u3 - VREF/NAME/conf/ -gt 2 = u3 gitolite3-3.6.1/src/commands/000077500000000000000000000000001241446647300160275ustar00rootroot00000000000000gitolite3-3.6.1/src/commands/D000077500000000000000000000105211241446647300161370ustar00rootroot00000000000000#!/bin/sh # ---------------------------------------------------------------------- # ADMINISTRATOR NOTES: # ---------------------------------------------------------------------- # - set TRASH_CAN in the rc if you don't like the default. It should be # relative to GL_REPO_BASE or an absolute value. It should also be on the # same filesystem as GL_REPO_BASE, otherwise the 'mv' will take too long. # - you could set TRASH_SUFFIX also but I recomend you leave it as it is # - run a cron job to delete old repos based on age (the TRASH_SUFFIX has a # timestamp); your choice how/how often you do that # - you can completely disable the 'rm' command by setting an rc variable # called D_DISABLE_RM to "1". # ---------------------------------------------------------------------- # ---------------------------------------------------------------------- # Usage: ssh git@host D # # The whimsically named "D" command deletes repos ("D" is a counterpart to the # "C" permission which lets you create repos. Which also means that, just # like "C", it only works for wild repos). # # There are two kinds of deletions: 'rm' removes a repo completely, while # 'trash' moves it to a trashcan which can be recovered later (upto a time # limit that your admin will tell you). # # The 'rm', 'lock', and 'unlock' subcommands: # Initially, all repos are "locked" against 'rm'. The correct sequence is # ssh git@host D unlock repo # ssh git@host D rm repo # Since the initial condition is always locked, the "lock" command is # rarely used but it is there if you want it. # # The 'trash', 'list-trash', and 'restore' subcommands: # You can 'trash' a repo, which moves it to a special place: # ssh git@host D trash repo # You can then 'list-trash' # ssh git@host D list-trash # which prints something like # repo/2012-04-11_05:58:51 # allowing you to restore by saying # ssh git@host D restore repo/2012-04-11_05:58:51 die() { echo "$@" >&2; exit 1; } usage() { perl -lne 'print substr($_, 2) if /^# Usage/../^$/' < $0; exit 1; } [ -z "$1" ] && usage [ "$1" = "-h" ] && usage [ "$1" != "list-trash" ] && [ -z "$2" ] && usage [ -z "$GL_USER" ] && die GL_USER not set # ---------------------------------------------------------------------- cmd=$1 repo=$2 # ---------------------------------------------------------------------- RB=`gitolite query-rc GL_REPO_BASE`; cd $RB TRASH_CAN=`gitolite query-rc TRASH_CAN`; tcan=Trash; TRASH_CAN=${TRASH_CAN:-$tcan} TRASH_SUFFIX=`gitolite query-rc TRASH_SUFFIX`; tsuf=`date +%Y-%m-%d_%H:%M:%S`; TRASH_SUFFIX=${TRASH_SUFFIX:-$tsuf} # ---------------------------------------------------------------------- owner_or_die() { gitolite owns "$repo" || die You are not authorised } # ---------------------------------------------------------------------- if [ "$cmd" = "rm" ] then gitolite query-rc -q D_DISABLE_RM && die "sorry, 'unlock' and 'rm' are disabled" owner_or_die [ -f $repo.git/gl-rm-ok ] || die "'$repo' is locked!" rm -rf $repo.git echo "'$repo' is now gone!" elif [ "$cmd" = "lock" ] then owner_or_die rm -f $repo.git/gl-rm-ok echo "'$repo' is now locked" elif [ "$cmd" = "unlock" ] then gitolite query-rc -q D_DISABLE_RM && die "sorry, 'unlock' and 'rm' are disabled" owner_or_die touch $repo.git/gl-rm-ok echo "'$repo' is now unlocked" elif [ "$cmd" = "trash" ] then owner_or_die mkdir -p $TRASH_CAN/$repo 2>/dev/null || die "failed creating directory in trashcan" [ -d $TRASH_CAN/$repo/$TRASH_SUFFIX ] && die "try again in a few seconds..." mv $repo.git $TRASH_CAN/$repo/$TRASH_SUFFIX echo "'$repo' moved to trashcan" elif [ "$cmd" = "list-trash" ] then cd $TRASH_CAN 2>/dev/null || exit 0 find . -name gl-creator | sort | while read t do owner= owner=`cat "$t"` [ "$owner" = "$GL_USER" ] && dirname $t done | cut -c3- elif [ "$cmd" = "restore" ] then owner= owner=`cat $TRASH_CAN/$repo/gl-creator 2>/dev/null` [ "$owner" = "$GL_USER" ] || die "'$repo' is not yours!" cd $TRASH_CAN realrepo=`dirname $repo` [ -d $RB/$realrepo.git ] && die "'$realrepo' already exists" mv $repo $RB/$realrepo.git echo "'$repo' restored to '$realrepo'" else die "unknown subcommand '$cmd'" fi gitolite3-3.6.1/src/commands/access000077500000000000000000000111561241446647300172220ustar00rootroot00000000000000#!/usr/bin/perl -s use strict; use warnings; use lib $ENV{GL_LIBDIR}; use Gitolite::Rc; use Gitolite::Common; use Gitolite::Conf::Load; our ( $q, $s, $h ); # quiet, show, help =for usage Usage: gitolite access [-q|-s] Print access rights for arguments given. The string printed has the word DENIED in it if access was denied. With '-q', returns only an exit code (shell truth, not perl truth -- 0 is success). For '-s', see below. - repo: mandatory - user: mandatory - perm: defauts to '+'. Valid values: R, W, +, C, D, M - ref: defauts to 'any'. See notes below Notes: - ref: something like 'master', or 'refs/tags/v1.0', or even a VREF if you know what they look like. The 'any' ref is special -- it ignores deny rules, thus simulating gitolite's behaviour during the pre-git access check (see 'deny-rules' section in rules.html for details). - batch mode: see src/triggers/post-compile/update-git-daemon-access-list for a good example that shows how to test several repos in one invocation. This is orders of magnitude faster than running the command multiple times; you'll notice if you have more than a hundred or so repos. - '-s' shows the rules (conf file name, line number, and rule) that were considered and how they fared. =cut usage() if not @ARGV or $h; my ( $repo, $user, $aa, $ref ) = @ARGV; # default access is '+' $aa ||= '+'; # default ref is 'any' $ref ||= 'any'; # fq the ref if needed $ref =~ s(^)(refs/heads/) if $ref and $ref ne 'any' and $ref !~ m(^(refs|VREF)/); _die "invalid perm" if not( $aa and $aa =~ /^(R|W|\+|C|D|M|\^C)$/ ); _die "invalid ref name" if not( $ref and $ref =~ $REPONAME_PATT ); my $ret = ''; if ( $repo ne '%' and $user ne '%' ) { # single repo, single user; no STDIN $ret = access( $repo, $user, $aa, $ref ); show() if $s; if ( $ret =~ /DENIED/ ) { print "$ret\n" unless $q; exit 1; } print "$ret\n" unless $q; exit 0; } $repo = '' if $repo eq '%'; $user = '' if $user eq '%'; _die "'-q' and '-s' meaningless in pipe mode" if $q or $s; @ARGV = (); while (<>) { my @in = split; my $r = $repo || shift @in; my $u = $user || shift @in; $ret = access( $r, $u, $aa, $ref ); print "$r\t$u\t$ret\n"; } sub show { my $in = $rc{RULE_TRACE} or die "this should not happen!"; print STDERR "legend:"; print STDERR " d => skipped deny rule due to ref unknown or 'any', r => skipped due to refex not matching, p => skipped due to perm (W, +, etc) not matching, D => explicitly denied, A => explicitly allowed, F => denied due to fallthru (no rules matched) "; my %rule_info = read_ri($in); # get rule info data for all traced rules # this means conf filename, line number, and content of the line # the rule-trace info is a set of pairs of a number plus a string. Only # the last character in a string is valid (and has meanings shown above). # At the end there may be a final 'f' my @in = split ' ', $in; while (@in) { $in = shift @in; if ( $in =~ /^\d+$/ ) { my $res = shift @in or die "this should not happen either!"; my $m = chop($res); printf " %s %20s:%-6s %s\n", $m, $rule_info{$in}{fn}, $rule_info{$in}{ln}, $rule_info{$in}{cl}; } elsif ( $in eq 'F' ) { printf " %s %20s\n", $in, "(fallthru)"; } else { die "and finally, this also should not happen!"; } } print "\n"; } sub read_ri { my %rules = map { $_ => 1 } $_[0] =~ /(\d+)/g; # contains a series of rule numbers, each of which we must search in # $GL_ADMIN_BASE/.gitolite/conf/rule_info my %rule_info; for ( slurp( $ENV{GL_ADMIN_BASE} . "/conf/rule_info" ) ) { my ( $r, $f, $l ) = split ' ', $_; next unless $rules{$r}; $rule_info{$r}{fn} = $f; $rule_info{$r}{ln} = $l; $rule_info{$r}{cl} = conf_lines( $f, $l ); # a wee bit of optimisation, in case the rule_info file is huge and # what we want is up near the beginning delete $rules{$r}; last unless %rules; } return %rule_info; } { my %conf_lines; sub conf_lines { my ( $file, $line ) = @_; $line--; unless ( $conf_lines{$file} ) { $conf_lines{$file} = [ slurp( $ENV{GL_ADMIN_BASE} . "/conf/$file" ) ]; chomp( @{ $conf_lines{$file} } ); } return $conf_lines{$file}[$line]; } } gitolite3-3.6.1/src/commands/create000077500000000000000000000010261241446647300172170ustar00rootroot00000000000000#!/bin/bash # Usage: ssh git@host create # # Create wild repo. die() { echo "$@" >&2; exit 1; } usage() { perl -lne 'print substr($_, 2) if /^# Usage/../^$/' < $0; exit 1; } [ -z "$1" ] && usage [ -z "$2" ] || usage [ "$1" = "-h" ] && usage [ -z "$GL_USER" ] && die GL_USER not set # ---------------------------------------------------------------------- gitolite git-config -r $1 gitolite-options.default.roles | sort | cut -f3 | perl -pe 's/(\s)CREATOR(\s|$)/$1$ENV{GL_USER}$1/' | $GL_BINDIR/commands/perms -c "$@" gitolite3-3.6.1/src/commands/creator000077500000000000000000000016071241446647300174200ustar00rootroot00000000000000#!/usr/bin/perl use strict; use warnings; use lib $ENV{GL_LIBDIR}; use Gitolite::Rc; use Gitolite::Common; use Gitolite::Conf::Load; =for usage Usage: gitolite creator [-n] [] Print the creator name for the repo. A '-n' suppresses the newline. When an optional username is supplied, it checks if the user is the creator of the repo and returns an exit code (shell truth, 0 for success) instead of printing anything, which makes it possible to do this in shell: if gitolite creator someRepo someUser then ... =cut usage() if not @ARGV or $ARGV[0] eq '-h'; my $nl = "\n"; if ( $ARGV[0] eq '-n' ) { $nl = ''; shift; } my $repo = shift; my $user = shift || ''; my $creator = ''; $creator = creator($repo) if not repo_missing($repo); if ($user) { exit 0 if $creator eq $user; exit 1; } return ( $creator eq $user ) if $user; print "$creator$nl"; gitolite3-3.6.1/src/commands/desc000077500000000000000000000032161241446647300166750ustar00rootroot00000000000000#!/usr/bin/perl use strict; use warnings; use lib $ENV{GL_LIBDIR}; use Gitolite::Easy; =for usage Usage: ssh git@host desc ssh git@host desc Show or set description for repo. You need to have write access to the repo and the 'writer-is-owner' option must be set for the repo, or it must be a user-created ('wild') repo and you must be the owner. =cut usage() if not @ARGV or @ARGV < 1 or $ARGV[0] eq '-h'; my $repo = shift; my $text = join( " ", @ARGV ); my $file = 'description'; #<<< _die "you are not authorized" unless ( not $text and can_read($repo) ) or ( $text and owns($repo) ) or ( $text and can_write($repo) and ( $rc{WRITER_CAN_UPDATE_DESC} or option( $repo, 'writer-is-owner' ) ) ); #>>> $text ? textfile( file => $file, repo => $repo, text => $text ) : print textfile( file => $file, repo => $repo ); __END__ kernel.org needs 'desc' to be available to people who have "RW" or above, not just the "creator". In fact they need it for non-wild repos so there *is* no creator. To accommodate this, we created the WRITER_CAN_UPDATE_DESC rc variable. However, that has turned out to be a bit of a blunt instrument for people with different types of wild repos -- they don't want to apply this to all of them. It seems easier to do this as an option, so you may have it for one set of "repo ..." and not have it for others. And if you want it for the whole system you'd just put it under "repo @all". The new 'writer-is-owner' option is meant to cover desc, readme, and any other repo-specific text file, so it's also a blunt instrument, though in a different dimension :-) gitolite3-3.6.1/src/commands/fork000077500000000000000000000043221241446647300167170ustar00rootroot00000000000000#!/bin/sh # Usage: ssh git@host fork # # Forks repo1 to repo2. You must have read permissions on repo1, and create # ("C") permissions for repo2, which of course must not exist. # # A fork is functionally the same as cloning repo1 to a client and pushing it # to a new repo2. It's just a little more efficient, not just in network # traffic but because it uses git clone's "-l" option to share the object # store also, so it is likely to be almost instantaneous, regardless of how # big the repo actually is. die() { echo "$@" >&2; exit 1; } usage() { perl -lne 'print substr($_, 2) if /^# Usage/../^$/' < $0; exit 1; } [ -z "$1" ] && usage [ "$1" = "-h" ] && usage [ -z "$GL_USER" ] && die GL_USER not set # ---------------------------------------------------------------------- from=$1; shift to=$1; shift [ -z "$to" ] && usage gitolite access -q "$from" $GL_USER R any || die "'$from' does not exist or you are not allowed to read it" gitolite access -q "$to" $GL_USER ^C any || die "'$to' already exists or you are not allowed to create it" # ---------------------------------------------------------------------- # IMPORTANT NOTE: checking whether someone can create a repo is done as above. # However, make sure that the env var GL_USER is set, and that too to the same # value as arg-2 of the access command), otherwise it won't work. # Ideally, you'll leave such code to me. There's a reason ^C is not listed in # the help message for 'gitolite access'. # ---------------------------------------------------------------------- # clone $from to $to git clone --bare -l $GL_REPO_BASE/$from.git $GL_REPO_BASE/$to.git [ $? -ne 0 ] && exit 1 echo "$from forked to $to" >&2 # fix up creator, default role permissions (gl-perms), and hooks cd $GL_REPO_BASE/$to.git echo $GL_USER > gl-creator gitolite query-rc -q LOCAL_CODE && ln -sf `gitolite query-rc LOCAL_CODE`/hooks/common/* hooks ln -sf `gitolite query-rc GL_ADMIN_BASE`/hooks/common/* hooks # record where you came from echo "$from" > gl-forked-from # cache control, if rc says caching is on gitolite query-rc -q CACHE && perl -I$GL_LIBDIR -MGitolite::Cache -e "cache_control('flush', '$to')"; # trigger post_create gitolite trigger POST_CREATE $to $GL_USER fork gitolite3-3.6.1/src/commands/git-annex-shell000077500000000000000000000044541241446647300207630ustar00rootroot00000000000000#!/usr/bin/perl use lib $ENV{GL_LIBDIR}; use Gitolite::Easy; # This command requires unrestricted arguments, so add it to the ENABLE list # like this: # 'git-annex-shell ua', # This requires git-annex version 20111016 or newer. Older versions won't # be secure. use strict; use warnings; # ignore @ARGV and look at the original unmodified command my $cmd = $ENV{SSH_ORIGINAL_COMMAND}; # Expect commands like: # git-annex-shell 'configlist' '/~/repo' # git-annex-shell 'sendkey' '/~/repo' 'key' # The parameters are always single quoted, and the repo path is always # the second parameter. # Further parameters are not validated here (see below). die "bad git-annex-shell command: $cmd" unless $cmd =~ m#^(git-annex-shell '\w+' ')/\~/([0-9a-zA-Z][0-9a-zA-Z._\@/+-]*)('( .*|))$#; my $start = $1; my $repo = $2; my $end = $3; $repo =~ s/\.git$//; die "I dont like some of the characters in $repo\n" unless $repo =~ $Gitolite::Rc::REPONAME_PATT; die "I dont like absolute paths in $cmd\n" if $repo =~ /^\//; die "I dont like '..' paths in $cmd\n" if $repo =~ /\.\./; # Modify $cmd, fixing up the path to the repo to include GL_REPO_BASE. my $newcmd = "$start$rc{GL_REPO_BASE}/$repo$end"; # Rather than keeping track of which git-annex-shell commands # require write access and which are readonly, we tell it # when readonly access is needed. if ( can_write($repo) ) { } elsif ( can_read($repo) ) { $ENV{GIT_ANNEX_SHELL_READONLY} = 1; } else { die "$repo $ENV{GL_USER} DENIED\n"; } # Further limit git-annex-shell to safe commands (avoid it passing # unknown commands on to git-shell) $ENV{GIT_ANNEX_SHELL_LIMITED} = 1; # Note that $newcmd does *not* get evaluated by the unix shell. # Instead it is passed as a single parameter to git-annex-shell for # it to parse and handle the command. This is why we do not need to # fully validate $cmd above. Gitolite::Common::gl_log( $ENV{SSH_ORIGINAL_COMMAND} ); exec "git-annex-shell", "-c", $newcmd; __END__ INSTRUCTIONS... (NEED TO BE VALIDATED BY SOMEONE WHO KNOWS GIT-ANNEX WELL). based on http://git-annex.branchable.com/tips/using_gitolite_with_git-annex/ ONLY VARIATIONS FROM THAT PAGE ARE WRITTEN HERE. setup * in the ENABLE list in the rc file, add an entry like this: 'git-annex-shell ua', That should be it; everything else should be as in that page. gitolite3-3.6.1/src/commands/git-config000077500000000000000000000052721241446647300200110ustar00rootroot00000000000000#!/usr/bin/perl use strict; use warnings; use Getopt::Long; use lib $ENV{GL_LIBDIR}; use Gitolite::Rc; use Gitolite::Common; use Gitolite::Conf::Load; =for usage Usage: gitolite git-config [-n] [-q] [-r] Print git config keys and values for the given repo. The key is either a full key, or, if '-r' is supplied, a regex that is applied to all available keys. -q exit code only (shell truth; 0 is success) -n suppress trailing newline when used as key (not pattern) -r treat key as regex pattern (unanchored) -ev print keys with empty values also (see below) Examples: gitolite git-config repo gitweb.owner gitolite git-config -q repo gitweb.owner gitolite git-config -r repo gitweb Notes: 1. When the key is treated as a pattern, prints: reponamekeyvalue Otherwise the output is just the value. 2. By default, keys with empty values (specified as "" in the conf file) are treated as non-existant. Using '-ev' will print those keys also. Note that this only makes sense when the key is treated as a pattern, where such keys are printed as: reponamekey 3. Finally, see the advanced use section of 'gitolite access -h' -- you can do something similar here also: gitolite list-phy-repos | gitolite git-config -r % gitweb\\. | cut -f1 > ~/projects.list =cut usage() if not @ARGV; my ( $help, $nonl, $quiet, $regex, $ev ) = (0) x 5; GetOptions( 'n' => \$nonl, 'q' => \$quiet, 'r' => \$regex, 'h' => \$help, 'ev' => \$ev, ) or usage(); my ( $repo, $key ) = @ARGV; usage() unless $key; my $ret = ''; if ( $repo ne '%' and $key ne '%' ) { # single repo, single key; no STDIN $key = "^\Q$key\E\$" unless $regex; $ret = git_config( $repo, $key, $ev ); # if the key is not a regex, it should match at most one item _die "found more than one entry for '$key'" if not $regex and scalar( keys %$ret ) > 1; # unlike access, there's nothing to print if we don't find any matching keys exit 1 unless %$ret; if ($regex) { map { print "$repo\t$_\t" . $ret->{$_} . "\n" } sort keys %$ret unless $quiet; } else { map { print $ret->{$_} . ( $nonl ? "" : "\n" ) } sort keys %$ret unless $quiet; } exit 0; } $repo = '' if $repo eq '%'; $key = '' if $key eq '%'; _die "'-q' doesn't go with using a pipe" if $quiet; @ARGV = (); while (<>) { my @in = split; my $r = $repo || shift @in; my $k = $key || shift @in; $k = "^\Q$k\E\$" unless $regex; $ret = git_config( $r, $k, $ev ); next unless %$ret; map { print "$r\t$_\t" . $ret->{$_} . "\n" } sort keys %$ret; } gitolite3-3.6.1/src/commands/help000077500000000000000000000021061241446647300167040ustar00rootroot00000000000000#!/usr/bin/perl use strict; use warnings; use lib $ENV{GL_LIBDIR}; use Gitolite::Rc; use Gitolite::Common; =for usage Usage: ssh git@host help # via ssh gitolite help # directly on server command line Prints a list of custom commands available at this gitolite installation. Each command has its own help, accessed by passing it '-h' again. =cut usage() if @ARGV; print greeting(); my $user = $ENV{GL_USER} || ''; print "list of " . ( $user ? "remote" : "gitolite" ) . " commands available:\n\n"; my %list = ( list_x( $ENV{GL_BINDIR} ), list_x( $rc{LOCAL_CODE} || '' ) ); for ( sort keys %list ) { print "\t$list{$_}" if $ENV{D}; print "\t$_\n" if not $user or $rc{COMMANDS}{$_}; } print "\n"; print "$rc{SITE_INFO}\n" if $rc{SITE_INFO}; exit 0; # ------------------------------------------------------------------------------ sub list_x { my $d = shift; return unless $d; return unless -d "$d/commands"; _chdir "$d/commands"; return map { $_ => $d } grep { -x $_ } map { chomp; s(^./)(); $_ } `find . -type f -o -type l|sort`; } gitolite3-3.6.1/src/commands/htpasswd000077500000000000000000000024641241446647300176200ustar00rootroot00000000000000#!/usr/bin/perl use strict; use warnings; use lib $ENV{GL_LIBDIR}; use Gitolite::Rc; use Gitolite::Common; =for usage Usage: ssh git@host htpasswd Sets your htpasswd, assuming your admin has enabled it. (Admins: You need to add HTPASSWD_FILE to the rc file, pointing to an existing, writable, but possibly an initially empty, file, as well as adding 'htpasswd' to the ENABLE list). =cut # usage and sanity checks usage() if @ARGV and $ARGV[0] eq '-h'; $ENV{GL_USER} or _die "GL_USER not set"; my $htpasswd_file = $rc{HTPASSWD_FILE} || ''; die "htpasswd not enabled\n" unless $htpasswd_file; die "$htpasswd_file doesn't exist or is not writable\n" unless -w $htpasswd_file; # prompt $|++; print <; $password =~ s/[\n\r]*$//; die "empty passwords are not allowed\n" unless $password; my $res = system( "htpasswd", "-mb", $htpasswd_file, $ENV{GL_USER}, $password ); die "htpasswd command seems to have failed with return code: $res.\n" if $res; gitolite3-3.6.1/src/commands/info000077500000000000000000000064231241446647300167150ustar00rootroot00000000000000#!/usr/bin/perl use strict; use warnings; use Getopt::Long; use lib $ENV{GL_LIBDIR}; use Gitolite::Rc; use Gitolite::Common; use Gitolite::Conf::Load; =for args Usage: gitolite info [-lc] [-ld] [-json] [] List all existing repos you can access, as well as repo name patterns you can create repos from (if any). '-lc' lists creators as an additional field at the end. '-ld' lists description as an additional field at the end. '-json' produce JSON output instead of normal output The optional pattern is an unanchored regex that will limit the repos searched, in both cases. It might speed up things a little if you have more than a few thousand repos. =cut # these are globals my ( $lc, $ld, $json, $patt ) = args(); my %out; # holds info to be json'd $ENV{GL_USER} or _die "GL_USER not set"; if ($json) { greeting(\%out); } else { print greeting(); } print_patterns(); # repos he can create for himself print_phy_repos(); # repos already created if ( $rc{SITE_INFO} ) { $json ? $out{SITE_INFO} = $rc{SITE_INFO} : print "\n$rc{SITE_INFO}\n"; } print JSON::to_json( \%out, { utf8 => 1, pretty => 1 } ) if $json; # ---------------------------------------------------------------------- sub args { my ( $lc, $ld, $json, $patt ) = ( '', '', '', '' ); my $help = ''; GetOptions( 'lc' => \$lc, 'ld' => \$ld, 'json' => \$json, 'h' => \$help, ) or usage(); usage() if @ARGV > 1 or $help; $patt = shift @ARGV || '.'; require JSON if $json; return ( $lc, $ld, $json, $patt ); } sub print_patterns { my ( $repos, @aa ); my $lm = \&Gitolite::Conf::Load::list_members; # find repo patterns only, call them with ^C flag included @$repos = grep { !/$REPONAME_PATT/ } map { /^@/ ? @{ $lm->($_) } : $_ } @{ lister_dispatch('list-repos')->() }; @aa = qw(R W ^C); listem( $repos, '', '', @aa ); # but squelch the 'lc' and 'ld' flags for these } sub print_phy_repos { my ( $repos, @aa ); # now get the actual repos and get R or W only _chdir( $rc{GL_REPO_BASE} ); $repos = list_phy_repos(1); @aa = qw(R W); listem( $repos, $lc, $ld, @aa ); } sub listem { my ( $repos, $lc, $ld, @aa ) = @_; my $creator = ''; for my $repo (@$repos) { next unless $repo =~ /$patt/; my $perm = ''; $creator = creator($repo) if $lc; my $desc = ''; for my $d ("$ENV{GL_REPO_BASE}/$repo.git/description") { next unless $ld and -r $d; $desc = slurp($d); chomp($desc); } for my $aa (@aa) { my $ret = access( $repo, $ENV{GL_USER}, $aa, 'any' ); $perm .= ( $ret =~ /DENIED/ ? " " : " $aa" ); } $perm =~ s/\^//; next unless $perm =~ /\S/; if ($json) { $out{repos}{$repo}{creator} = $creator if $lc; $out{repos}{$repo}{description} = $desc if $ld; $out{repos}{$repo}{perms} = _hash($perm); next; } print "$perm\t$repo"; print "\t$creator" if $lc; print "\t$desc" if $ld; print "\n"; } } sub _hash { my $in = shift; my %out = map { $_ => 1 } ( $in =~ /(\S)/g ); return \%out; } gitolite3-3.6.1/src/commands/list-dangling-repos000077500000000000000000000034121241446647300216370ustar00rootroot00000000000000#!/usr/bin/perl use strict; use warnings; use lib $ENV{GL_LIBDIR}; use Gitolite::Common; use Gitolite::Conf::Load; =for usage Usage: gitolite list-dangling-repos List all existing repos that no one can access remotely any more. They could be normal repos that were taken out of "repo" statements in the conf file, or wildcard repos whose matching "wild" pattern was taken out or changed so it no longer matches. I would advise caution if you use this as a basis for deleting repos from the file system. A bug in this program could cause you to lose important data! =cut usage() if @ARGV and $ARGV[0] eq '-h'; # get the two lists we need. %repos is the list of repos in "repo" statements # in the conf file. %phy_repos is the list of actual repos on disk. Our job # is to cull %phy_repos of all keys that have a matching key in %repos, where # "matching" means "string equal" or "regex match". my %repos = map { chomp; $_ => 1 } `gitolite list-repos`; for my $r ( grep /^@/, keys %repos ) { map { chomp; $repos{$_} = 1; } `gitolite list-members $r`; } my %phy_repos = map { chomp; $_ => 1 } `gitolite list-phy-repos`; # Remove exact matches. But for repo names like "gtk+", you could have # collapsed this into the next step (the regex match). for my $pr ( keys %phy_repos ) { next unless exists $repos{$pr}; delete $repos{$pr}; delete $phy_repos{$pr}; } # Remove regex matches. for my $pr ( keys %phy_repos ) { my $matched = 0; my $pr2 = Gitolite::Conf::Load::generic_name($pr); for my $r ( keys %repos ) { if ( $pr =~ /^$r$/ or $pr2 =~ /^$r$/ ) { $matched = 1; next; } } delete $phy_repos{$pr} if $matched; } # what's left in %phy_repos are dangling repos. print join( "\n", sort keys %phy_repos ), "\n"; gitolite3-3.6.1/src/commands/lock000077500000000000000000000077541241446647300167220ustar00rootroot00000000000000#!/usr/bin/perl use strict; use warnings; use Getopt::Long; use lib $ENV{GL_LIBDIR}; use Gitolite::Rc; use Gitolite::Common; use Gitolite::Conf::Load; # gitolite command to lock and unlock (binary) files and deal with locks. =for usage Usage: ssh git@host lock -l # lock a file ssh git@host lock -u # unlock a file ssh git@host lock --break # break someone else's lock ssh git@host lock -ls # list locked files for repo See doc/locking.mkd for other details. =cut usage() if not @ARGV or $ARGV[0] eq '-h'; $ENV{GL_USER} or _die "GL_USER not set"; my $op = ''; $op = 'lock' if $ARGV[0] eq '-l'; $op = 'unlock' if $ARGV[0] eq '-u'; $op = 'break' if $ARGV[0] eq '--break'; $op = 'list' if $ARGV[0] eq '-ls'; usage() if not $op; shift; my $repo = shift; _die "You are not authorised" if access( $repo, $ENV{GL_USER}, 'W', 'any' ) =~ /DENIED/; _die "You are not authorised" if $op eq 'break' and access( $repo, $ENV{GL_USER}, '+', 'any' ) =~ /DENIED/; my $file = shift || ''; usage() if $op ne 'list' and not $file; _chdir( $ENV{GL_REPO_BASE} ); _chdir("$repo.git"); _die "aborting, file '$file' not found in any branch" if $file and not object_exists($file); my $ff = "gl-locks"; if ( $op eq 'lock' ) { f_lock( $repo, $file ); } elsif ( $op eq 'unlock' ) { f_unlock( $repo, $file ); } elsif ( $op eq 'break' ) { f_break( $repo, $file ); } elsif ( $op eq 'list' ) { f_list($repo); } # ---------------------------------------------------------------------- # For a given path, return 1 if object exists in any branch, 0 if not. # This is to prevent locking invalid objects. sub object_exists { my $file = shift; my @branches = `git for-each-ref refs/heads '--format=%(refname)'`; foreach my $b (@branches) { chomp($b); system("git cat-file -e $b:$file 2>/dev/null") or return 1; # note that with system(), the return value is "shell truth", so # you check for success with "or", not "and" } return 0; # report object not found } # ---------------------------------------------------------------------- # everything below assumes we have already chdir'd to "$repo.git". Also, $ff # is used as a global. sub f_lock { my ( $repo, $file ) = @_; my %locks = get_locks(); _die "'$file' locked by '$locks{$file}{USER}' since " . localtime( $locks{$file}{TIME} ) if $locks{$file}{USER}; $locks{$file}{USER} = $ENV{GL_USER}; $locks{$file}{TIME} = time; put_locks(%locks); } sub f_unlock { my ( $repo, $file ) = @_; my %locks = get_locks(); _die "'$file' not locked by '$ENV{GL_USER}'" if ( $locks{$file}{USER} || '' ) ne $ENV{GL_USER}; delete $locks{$file}; put_locks(%locks); } sub f_break { my ( $repo, $file ) = @_; my %locks = get_locks(); _die "'$file' was not locked" unless $locks{$file}; push @{ $locks{BREAKS} }, time . " $ENV{GL_USER} $locks{$file}{USER} $locks{$file}{TIME} $file"; delete $locks{$file}; put_locks(%locks); } sub f_list { my $repo = shift; my %locks = get_locks(); print "\n# locks held:\n\n"; map { print "$locks{$_}{USER}\t$_\t(" . scalar( localtime( $locks{$_}{TIME} ) ) . ")\n" } grep { $_ ne 'BREAKS' } sort keys %locks; print "\n# locks broken:\n\n"; for my $b ( @{ $locks{BREAKS} } ) { my ( $when, $who, $whose, $how_old, $what ) = split ' ', $b; print "$who\t$what\t(" . scalar( localtime($when) ) . ")\t(locked by $whose at " . scalar( localtime($how_old) ) . ")\n"; } } sub get_locks { if ( -f $ff ) { our %locks; my $t = slurp($ff); eval $t; _die "do '$ff' failed with '$@', contact your administrator" if $@; return %locks; } return (); } sub put_locks { my %locks = @_; use Data::Dumper; $Data::Dumper::Indent = 1; $Data::Dumper::Sortkeys = 1; my $dumped_data = Data::Dumper->Dump( [ \%locks ], [qw(*locks)] ); _print( $ff, $dumped_data ); } gitolite3-3.6.1/src/commands/mirror000077500000000000000000000122411241446647300172670ustar00rootroot00000000000000#!/usr/bin/perl use strict; use warnings; my $tid; BEGIN { $tid = $ENV{GL_TID} || 0; delete $ENV{GL_TID}; } use lib $ENV{GL_LIBDIR}; use Gitolite::Rc; use Gitolite::Common; use Gitolite::Conf::Load; =for usage Usage 1: gitolite mirror push Usage 2: ssh git@master-server mirror push Forces a push of one repo to one slave. Usage 1 is directly on the master server. Nothing is checked; if the slave accepts it, the push happens, even if the slave is not in any slaves option. This is how you do delayed or lagged pushes to servers that do not need real-time updates or have bandwidth/connectivity issues. Usage 2 can be initiated by *any* user who has *any* gitolite access to the master server, but it checks that the slave is in one of the slaves options before doing the push. MIRROR STATUS: To find the status of the last mirror push to any slave, run the same command except with 'status' instead of 'push'. With usage 1, you can use the special name "all" to get the status of all slaves for the given repo. (Admins wishing to find the status of all slaves for ALL repos will have to script it using the output of "gitolite list-phy-repos".) SERVER LIST: 'gitolite mirror list master ' and 'gitolite mirror list slaves ' will show you the name of the master server, and list the slave servers, for the repo. They only work on the server command line (any server), but not remotely (from a normal user). =cut usage() if not @ARGV or $ARGV[0] eq '-h'; _die "HOSTNAME not set" if not $rc{HOSTNAME}; my ( $cmd, $host, $repo ) = @ARGV; usage() if not $repo; if ( $cmd eq 'push' ) { valid_slave( $host, $repo ) if exists $ENV{GL_USER}; # will die if host not in slaves for repo trace( 1, "TID=$tid host=$host repo=$repo", "gitolite mirror push started" ); _chdir( $rc{GL_REPO_BASE} ); _chdir("$repo.git"); if ( -f "gl-creator" ) { # try to propagate the wild repo, including creator name and gl-perms my $creator = `cat gl-creator`; chomp($creator); trace( 1, `cat gl-perms 2>/dev/null | ssh $host CREATOR=$creator perms -c \\'$repo\\' 2>/dev/null` ); } my $errors = 0; my $glss = ''; for (`git push --mirror $host:$repo 2>&1`) { $errors = 1 if $?; print STDERR "$_" if -t STDERR or exists $ENV{GL_USER}; $glss .= $_; chomp; if (/FATAL/) { $errors = 1; gl_log( 'mirror', $_ ); } else { trace( 1, "mirror: $_" ); } } # save the mirror push status for this slave if the word 'fatal' is found, # else remove the status file. We don't store "success" output messages; # you can always get those from the log files if you really need them. if ( $glss =~ /fatal/i ) { my $glss_prefix = Gitolite::Common::gen_ts() . "\t$ENV{GL_TID}\t"; $glss =~ s/^/$glss_prefix/gm; _print("gl-slave-$host.status", $glss); } else { unlink "gl-slave-$host.status"; } exit $errors; } elsif ($cmd eq 'status') { valid_slave( $host, $repo ) if exists $ENV{GL_USER}; # will die if host not in slaves for repo _chdir( $rc{GL_REPO_BASE} ); _chdir("$repo.git"); $host = '*' if $host eq 'all'; map { print_status($_) } sort glob("gl-slave-$host.status"); } else { # strictly speaking, we could allow some of the possible commands remotely # also, at least for admins. However, these commands are mainly intended # for server-side scripting so I don't care. usage() if $ENV{GL_USER}; server_side_commands(@ARGV); } # ---------------------------------------------------------------------- sub valid_slave { my ( $host, $repo ) = @_; _die "invalid repo '$repo'" unless $repo =~ $REPONAME_PATT; my %list = repo_slaves($repo); _die "'$host' not a valid slave for '$repo'" unless $list{$host}; } sub repo_slaves { my $repo = shift; my $ref = git_config( $repo, "^gitolite-options\\.mirror\\.slaves.*" ); my %list = map { $_ => 1 } map { split } values %$ref; return %list; } sub repo_master { my $repo = shift; my $ref = git_config( $repo, "^gitolite-options\\.mirror\\.master\$" ); my @list = map { split } values %$ref; _die "'$repo' seems to have more than one master" if @list > 1; return $list[0] || ''; } sub print_status { my $file = shift; return unless -f $file; my $slave = $1 if $file =~ /^gl-slave-(.+)\.status$/; print "----------\n"; print "WARNING: previous mirror push to host '$slave' failed, status is:\n"; print slurp($file); print "----------\n"; } # ---------------------------------------------------------------------- # server side commands. Very little error checking. # gitolite mirror list master # gitolite mirror list slaves sub server_side_commands { if ( $cmd eq 'list' ) { if ( $host eq 'master' ) { say repo_master($repo); } elsif ( $host eq 'slaves' ) { my %list = repo_slaves($repo); say join( " ", sort keys %list ); } else { _die "gitolite mirror list master|slaves "; } } else { _die "invalid command"; } } gitolite3-3.6.1/src/commands/motd000077500000000000000000000030031241446647300167140ustar00rootroot00000000000000#!/usr/bin/perl use strict; use warnings; use lib $ENV{GL_LIBDIR}; use Gitolite::Easy; =for usage Usage: ssh git@host motd rm cat | ssh git@host motd set Remove or set the motd file for repo or the whole system. For a repo: you need to have write access to the repo and the 'writer-is-owner' option must be set for the repo, or it must be a user-created ('wild') repo and you must be the owner. For the whole system: you need to be an admin (have write access to the gitolite-admin repo). Use @all in place of the repo name. PLEASE NOTE that if you're using http mode, the motd will only appear for gitolite commands, not for normal git operations. This in turn means that only the system wide motd can be seen; repo level motd's never show up. =cut usage() if not @ARGV or @ARGV < 1 or $ARGV[0] eq '-h'; my $repo = shift; my $op = shift || ''; usage() if $op ne 'rm' and $op ne 'set'; my $file = "gl-motd"; #<<< _die "you are not authorized" unless ( $repo eq '@all' and is_admin() ) or ( $repo ne '@all' and owns($repo) ) or ( $repo ne '@all' and can_write($repo) and option( $repo, 'writer-is-owner' ) ); #>>> my @out = $repo eq '@all' ? ( dir => $rc{GL_ADMIN_BASE} ) : ( repo => $repo ); if ( $op eq 'rm' ) { $repo eq '@all' ? unlink "$rc{GL_ADMIN_BASE}/$file" : unlink "$rc{GL_REPO_BASE}/$repo.git/$file"; } elsif ( $op eq 'set' ) { textfile( file => $file, @out, prompt => '' ); } else { print textfile( file => $file, @out, ); } gitolite3-3.6.1/src/commands/owns000077500000000000000000000006421241446647300167450ustar00rootroot00000000000000#!/usr/bin/perl use strict; use warnings; use lib $ENV{GL_LIBDIR}; use Gitolite::Easy; =for usage Usage: gitolite owns Checks if $GL_USER is an owner of the repo and returns an exit code (shell truth, 0 for success), which makes it possible to do this in shell: if gitolite owns someRepo then ... =cut usage() if not @ARGV or $ARGV[0] eq '-h'; my $repo = shift; exit not owns($repo); gitolite3-3.6.1/src/commands/perms000077500000000000000000000077051241446647300171140ustar00rootroot00000000000000#!/usr/bin/perl use strict; use warnings; use lib $ENV{GL_LIBDIR}; use Gitolite::Rc; use Gitolite::Common; use Gitolite::Easy; =for usage Usage: ssh git@host perms -l ssh git@host perms - ssh git@host perms + List or set permissions for user-created ("wild") repo. The first usage shown will list the current contents of the permissions file. The other two will change permissions, adding or removing a user from a role. Examples: ssh git@host perms foo + READERS user1 ssh git@host perms foo + READERS user2 ssh git@host perms foo + READERS user3 ---- There is also a batch mode useful for scripting and bulk loading. Do not combine this with the +/- mode above. This mode also accepts an optional "-c" flag to create the repo if it does not already exist (assuming $GL_USER has permissions to create it). Examples: cat copy-of-backed-up-gl-perms | ssh git@host perms cat copy-of-backed-up-gl-perms | ssh git@host perms -c =cut usage() if not @ARGV or $ARGV[0] eq '-h'; $ENV{GL_USER} or _die "GL_USER not set"; my $generic_error = "repo does not exist, or you are not authorised"; my $list = 0; if ( $ARGV[0] eq '-l' ) { $list++; shift; getperms(@ARGV); # doesn't return } # auto-create the repo if -c passed and repo doesn't exist if ( $ARGV[0] eq '-c' ) { shift; my $repo = $ARGV[0] or usage(); _die "invalid repo '$repo'" unless $repo =~ $REPONAME_PATT; if ( not -d "$rc{GL_REPO_BASE}/$repo.git" ) { my $ret = Gitolite::Conf::Load::access( $repo, $ENV{GL_USER}, '^C', 'any' ); _die $generic_error if $ret =~ /DENIED/; require Gitolite::Conf::Store; Gitolite::Conf::Store->import; new_wild_repo( $repo, $ENV{GL_USER}, 'perms-c' ); gl_log( 'create', $repo, $ENV{GL_USER}, 'perms-c' ); } } my $repo = shift; setperms(@ARGV); # cache control if ($rc{CACHE}) { require Gitolite::Cache; Gitolite::Cache::cache_control('flush', $repo); } _system( "gitolite", "trigger", "POST_CREATE", $repo, $ENV{GL_USER}, 'perms' ); # ---------------------------------------------------------------------- sub getperms { my $repo = shift; _die $generic_error if not owns($repo); my $pf = "$rc{GL_REPO_BASE}/$repo.git/gl-perms"; print slurp($pf) if -f $pf; exit 0; } sub setperms { _die $generic_error if not owns($repo); my $pf = "$rc{GL_REPO_BASE}/$repo.git/gl-perms"; if ( not @_ ) { # legacy mode; pipe data in print STDERR "'batch' mode started, waiting for input (run with '-h' for details).\n"; print STDERR "Please hit Ctrl-C if you did not intend to do this.\n"; @ARGV = (); my @a; for (<>) { _die "Invalid role '$1'; check the rc file" if /(\S+)/ and not $rc{ROLES}{$1}; push @a, $_; } print STDERR "\n"; # make sure Ctrl-C gets caught _print( $pf, @a ); return; } _die "Invalid syntax. Please re-run with '-h' for detailed usage" if @_ != 3; my ( $op, $role, $user ) = @_; _die "Invalid syntax. Please re-run with '-h' for detailed usage" if $op ne '+' and $op ne '-'; _die "Invalid role '$role'; check the rc file" if not $rc{ROLES}{$role}; _die "Invalid user '$user'" if not $user =~ $USERNAME_PATT; my $text = ''; my @text = slurp($pf) if -f $pf; my $present = grep { $_ eq "$role $user\n" } @text; if ( $op eq '-' ) { if ( not $present ) { _warn "'$role $user' was not present in file"; } else { @text = grep { $_ ne "$role $user\n" } @text; _print( $pf, @text ); } } else { if ($present) { _warn "'$role $user' already present in file"; } else { push @text, "$role $user\n"; @text = sort @text; _print( $pf, @text ); } } } gitolite3-3.6.1/src/commands/print-default-rc000077500000000000000000000001631241446647300211350ustar00rootroot00000000000000#!/usr/bin/perl use strict; use warnings; use lib $ENV{GL_LIBDIR}; use Gitolite::Rc; print glrc('default-text'); gitolite3-3.6.1/src/commands/push000077500000000000000000000000731241446647300167340ustar00rootroot00000000000000#!/bin/sh export GL_BYPASS_ACCESS_CHECKS=1 git push "$@" gitolite3-3.6.1/src/commands/readme000077500000000000000000000031321241446647300172110ustar00rootroot00000000000000#!/usr/bin/perl use strict; use warnings; use lib $ENV{GL_LIBDIR}; use Gitolite::Easy; # README.html files work similar to "description" files. For further # information see # https://www.kernel.org/pub/software/scm/git/docs/gitweb.html # under "Per-repository gitweb configuration". =for usage Usage: ssh git@host readme ssh git@host readme rm cat | ssh git@host readme set Show, remove or set the README.html file for repo. You need to have write access to the repo and the 'writer-is-owner' option must be set for the repo, or it must be a user-created ('wild') repo and you must be the owner. =cut usage() if not @ARGV or @ARGV < 1 or $ARGV[0] eq '-h'; my $repo = shift; my $op = shift || ''; usage() if $op and $op ne 'rm' and $op ne 'set'; my $file = 'README.html'; #<<< _die "you are not authorized" unless ( not $op and can_read($repo) ) or ( $op and owns($repo) ) or ( $op and can_write($repo) and option( $repo, 'writer-is-owner' ) ); #>>> if ( $op eq 'rm' ) { unlink "$rc{GL_REPO_BASE}/$repo.git/$file"; } elsif ( $op eq 'set' ) { textfile( file => $file, repo => $repo, prompt => '' ); } else { print textfile( file => $file, repo => $repo ); } __END__ The WRITER_CAN_UPDATE_README option is gone now; it applies to all the repos in the system. Much better to add 'option writer-is-owner = 1' to repos or repo groups that you want this to apply to. This option is meant to cover desc, readme, and any other repo-specific text file, so it's also a blunt instrument, though in a different dimension :-) gitolite3-3.6.1/src/commands/rsync000077500000000000000000000076411241446647300171230ustar00rootroot00000000000000#!/usr/bin/perl use strict; use warnings; use lib $ENV{GL_LIBDIR}; use Gitolite::Easy; =for admins BUNDLE SUPPORT (1) For each repo in gitolite.conf for which you want bundle support (or '@all', if you wish), add the following line: option bundle = 1 Or you can say: option bundle.ttl = A bundle file that is more than seconds old (default value 86400, i.e., 1 day) is recreated on the next bundle request. Increase this if your repo is not terribly active. Note: a bundle file is also deleted and recreated if it contains a ref that was then either deleted or rewound in the repo. This is checked on every invocation. (2) Add 'rsync' to the ENABLE list in the rc file GENERIC RSYNC SUPPORT TBD =cut =for usage rsync helper for gitolite BUNDLE SUPPORT Admins: see src/commands/rsync for setup instructions Users: rsync -P git@host:repo.bundle . # downloads a file called ".bundle"; repeat as # needed till the whole thing is downloaded git clone repo.bundle repo cd repo git remote set-url origin git@host:repo git fetch origin # and maybe git pull, etc. to freshen the clone GENERIC RSYNC SUPPORT TBD =cut usage() if not @ARGV or $ARGV[0] eq '-h'; # rsync driver program. Several things can be done later, but for now it # drives just the 'bundle' transfer. if ( $ENV{SSH_ORIGINAL_COMMAND} =~ /^rsync --server --sender (-[-\w=.]+ )+\. (\S+)\.bundle$/ ) { my $repo = $2; $repo =~ s/\.git$//; # all errors have the same message to avoid leaking info can_read($repo) or _die "you are not authorised"; my %config = config( $repo, "gitolite-options.bundle" ) or _die "you are not authorised"; my $ttl = $config{'gitolite-options.bundle.ttl'} || 86400; # in seconds (default 1 day) my $bundle = bundle_create( $repo, $ttl ); $ENV{SSH_ORIGINAL_COMMAND} =~ s( \S+\.bundle)( $bundle); trace( 1, "rsync bundle", $ENV{SSH_ORIGINAL_COMMAND} ); Gitolite::Common::_system( split ' ', $ENV{SSH_ORIGINAL_COMMAND} ); exit 0; } _warn "invalid rsync command '$ENV{SSH_ORIGINAL_COMMAND}'"; usage(); # ---------------------------------------------------------------------- # helpers # ---------------------------------------------------------------------- sub bundle_create { my ( $repo, $ttl ) = @_; my $bundle = "$repo.bundle"; $bundle =~ s(.*/)(); my $recreate = 0; my ( %b, %r ); if ( -f $bundle ) { %b = map { chomp; reverse split; } `git ls-remote --heads --tags $bundle`; %r = map { chomp; reverse split; } `git ls-remote --heads --tags .`; for my $ref ( sort keys %b ) { my $mtime = ( stat $bundle )[9]; if ( time() - $mtime > $ttl ) { trace( 1, "bundle too old" ); $recreate++; last; } if ( not $r{$ref} ) { trace( 1, "ref '$ref' deleted in repo" ); $recreate++; last; } if ( $r{$ref} eq $b{$ref} ) { # same on both sides; ignore delete $r{$ref}; delete $b{$ref}; next; } `git rev-list --count --left-right $b{$ref}...$r{$ref}` =~ /^(\d+)\s+(\d+)$/ or _die "git too old"; if ($1) { trace( 1, "ref '$ref' rewound in repo" ); $recreate++; last; } } } else { trace( 1, "no bundle found" ); $recreate++; } return $bundle if not $recreate; trace( 1, "creating bundle for '$repo'" ); -f $bundle and ( unlink $bundle or die "a horrible death" ); system("git bundle create $bundle --branches --tags >&2"); return $bundle; } sub trace { Gitolite::Common::trace(@_); } gitolite3-3.6.1/src/commands/sshkeys-lint000077500000000000000000000122121241446647300204100ustar00rootroot00000000000000#!/usr/bin/perl use strict; use warnings; # complete rewrite of the sshkeys-lint program. Usage has changed, see # usage() function or run without arguments. use Getopt::Long; my $admin = 0; my $quiet = 0; my $help = 0; GetOptions( 'admin|a=s' => \$admin, 'quiet|q' => \$quiet, 'help|h' => \$help ); use Data::Dumper; $Data::Dumper::Deepcopy = 1; $|++; my $in_gl_section = 0; my $warnings = 0; sub dbg { use Data::Dumper; for my $i (@_) { print STDERR "DBG: " . Dumper($i); } } sub msg { my $warning = shift; return if $quiet and not $warning; $warnings++ if $warning; print "sshkeys-lint: " . ( $warning ? "WARNING: " : "" ) . $_ for @_; } usage() if $help; our @pubkeyfiles = @ARGV; @ARGV = (); my $kd = "$ENV{HOME}/.gitolite/keydir"; if ( not @pubkeyfiles ) { chomp( @pubkeyfiles = `find $kd -type f -name "*.pub" | sort` ); } if ( -t STDIN ) { @ARGV = ("$ENV{HOME}/.ssh/authorized_keys"); } # ------------------------------------------------------------------------ my @authkeys; my %seen_fprints; my %pkf_by_fp; msg 0, "==== checking authkeys file:\n"; fill_authkeys(); # uses up STDIN if ($admin) { my $fp = fprint("$admin.pub"); my $fpu = ( $fp && $seen_fprints{$fp}{user} || 'no access' ); # dbg("fpu = $fpu, admin=$admin"); #<<< die "\t\t*** FATAL ***\n" . "$admin.pub maps to $fpu, not $admin.\n" . "You will not be able to access gitolite with this key.\n" . "Look for the 'ssh troubleshooting' link in http://gitolite.com/gitolite/ssh.html.\n" if $fpu ne "user $admin"; #>>> } msg 0, "==== checking pubkeys:\n" if @pubkeyfiles; for my $pkf (@pubkeyfiles) { # get the short name for the pubkey file ( my $pkfsn = $pkf ) =~ s(^$kd/)(); my $fp = fprint($pkf); next unless $fp; msg 1, "$pkfsn appears to be a COPY of $pkf_by_fp{$fp}\n" if $pkf_by_fp{$fp}; $pkf_by_fp{$fp} ||= $pkfsn; my $fpu = ( $seen_fprints{$fp}{user} || 'no access' ); msg 0, "$pkfsn maps to $fpu\n"; } if ($warnings) { print "\n$warnings warnings found\n"; } exit $warnings; # ------------------------------------------------------------------------ sub fill_authkeys { while (<>) { my $seq = $.; next if ak_comment($_); # also sets/clears $in_gl_section global my $fp = fprint($_); my $user = user($_); check( $seq, $fp, $user ); $authkeys[$seq]{fprint} = $fp; $authkeys[$seq]{ustatus} = $user; } } sub check { my ( $seq, $fp, $user ) = @_; msg 1, "line $seq, $user key found *outside* gitolite section!\n" if $user =~ /^user / and not $in_gl_section; msg 1, "line $seq, $user key found *inside* gitolite section!\n" if $user !~ /^user / and $in_gl_section; if ( $seen_fprints{$fp} ) { #<<< msg 1, "authkeys line $seq ($user) will be ignored by sshd; " . "same key found on line " . $seen_fprints{$fp}{seq} . " (" . $seen_fprints{$fp}{user} . ")\n"; return; #>>> } $seen_fprints{$fp}{seq} = $seq; $seen_fprints{$fp}{user} = $user; } sub user { my $user = ''; $user ||= "user $1" if /^command=.*gitolite-shell (.*?)"/; $user ||= "unknown command" if /^command/; $user ||= "shell access" if /^ssh-(rsa|dss)/; return $user; } sub ak_comment { local $_ = shift; $in_gl_section = 1 if /^# gitolite start/; $in_gl_section = 0 if /^# gitolite end/; die "gitosis? what's that?\n" if /^#.*gitosis/; return /^\s*(#|$)/; } sub fprint { local $_ = shift; my ( $fh, $tempfn, $in ); if ( /ssh-(dss|rsa) / || /ecdsa-/ ) { # an actual key was passed. Since ssh-keygen requires an actual file, # make a temp file to take the data and pass on to ssh-keygen s/^.* (ssh-dss|ssh-rsa|ecdsa-\S+)/$1/; use File::Temp qw(tempfile); ( $fh, $tempfn ) = tempfile(); $in = $tempfn; print $fh $_; close $fh; } else { # a filename was passed $in = $_; } # dbg("in = $in"); -f $in or die "file not found: $in\n"; open( $fh, "ssh-keygen -l -f $in |" ) or die "could not fork: $!\n"; my $fp = <$fh>; # dbg("fp = $fp"); close $fh; unlink $tempfn if $tempfn; warn "$fp\n" unless $fp =~ /([0-9a-f][0-9a-f](:[0-9a-f][0-9a-f])+)/; return $1; } # ------------------------------------------------------------------------ sub usage { print < baz.pub $user =~ s/(\@[^.]+)?\.pub$//; # baz.pub, baz@home.pub -> baz next unless $user eq $gl_user or $user =~ /^zzz-marked-for-...-$gl_user/; if ( $user =~ m(^zzz-marked-for-add-) ) { push @marked_for_add, $pubkey; } elsif ( $user =~ m(^zzz-marked-for-del-) ) { push @marked_for_del, $pubkey; } else { push @pubkeys, $pubkey; } } # ---- # list mode; just do it and exit sub print_keylist { my ( $message, @list ) = @_; return unless @list; print "== $message ==\n"; my $count = 1; for (@list) { my $fp = fingerprint($_); s/zzz-marked(\/|-for-...-)//g; print $count++ . ": $fp : $_\n"; } } if ( $operation eq 'list' ) { print "you have the following keys:\n"; print_keylist( "active keys", @pubkeys ); print_keylist( "keys marked for addition/replacement", @marked_for_add ); print_keylist( "keys marked for deletion", @marked_for_del ); print "\n\n"; exit; } # ---- # please see docs for details on how a user interacts with this if ( $keytype eq '' ) { # user logging in with a normal key die "valid operations: add, del, undo-add, confirm-del\n" unless $operation =~ /^(add|del|confirm-del|undo-add)$/; if ( $operation eq 'add' ) { print STDERR "please supply the new key on STDIN. (I recommend you don't try to do this interactively, but use a pipe)\n"; kf_add( $gl_user, $keyid, safe_stdin() ); } elsif ( $operation eq 'del' ) { kf_del( $gl_user, $keyid ); } elsif ( $operation eq 'confirm-del' ) { die "you dont have any keys marked for deletion\n" unless @marked_for_del; kf_confirm_del( $gl_user, $keyid ); } elsif ( $operation eq 'undo-add' ) { die "you dont have any keys marked for addition\n" unless @marked_for_add; kf_undo_add( $gl_user, $keyid ); } } elsif ( $keytype eq 'del' ) { # user is using a key that was marked for deletion. The only possible use # for this is that she changed her mind for some reason (maybe she marked # the wrong key for deletion) or is not able to get her client-side sshd # to stop using this key die "valid operations: undo-del\n" unless $operation eq 'undo-del'; # reinstate the key kf_undo_del( $gl_user, $keyid ); } elsif ( $keytype eq 'add' ) { die "valid operations: confirm-add\n" unless $operation eq 'confirm-add'; # user is trying to validate a key that has been previously marked for # addition. This isn't interactive, but it *could* be... if someone asked kf_confirm_add( $gl_user, $keyid ); } exit; # ---- # make a temp clone and switch to it our $TEMPDIR; BEGIN { $TEMPDIR = `mktemp -d -t tmp.XXXXXXXXXX`; } END { `/bin/rm -rf $TEMPDIR`; } sub cd_temp_clone { chomp($TEMPDIR); hushed_git( "clone", "$rb/gitolite-admin.git", "$TEMPDIR" ); chdir($TEMPDIR); my $hostname = `hostname`; chomp($hostname); hushed_git( "config", "--get", "user.email" ) and hushed_git( "config", "user.email", $ENV{USER} . "@" . $hostname ); hushed_git( "config", "--get", "user.name" ) and hushed_git( "config", "user.name", "$ENV{USER} on $hostname" ); } sub fingerprint { my $fp = `ssh-keygen -l -f $_[0]`; die "does not seem to be a valid pubkey\n" unless $fp =~ /(([0-9a-f]+:)+[0-9a-f]+ )/i; return $1; } sub safe_stdin { # read one line from STDIN my $data; my $ret = read STDIN, $data, 4096; # current pubkeys are approx 400 bytes so we go a little overboard die "could not read pubkey data" . ( defined($ret) ? "" : ": $!" ) . "\n" unless $ret; die "pubkey data seems to have more than one line\n" if $data =~ /\n./; return $data; } sub hushed_git { local (*STDOUT) = \*STDOUT; local (*STDERR) = \*STDERR; open( STDOUT, ">", "/dev/null" ); open( STDERR, ">", "/dev/null" ); system( "git", @_ ); } sub highlander { # there can be only one my ( $keyid, $die_if_empty, @a ) = @_; # too many? if ( @a > 1 ) { print STDERR " more than one key satisfies this condition, and I can't deal with that! The keys are: "; print STDERR "\t" . join( "\n\t", @a ), "\n\n"; exit 1; } # too few? die "no keys with " . ( $keyid || "empty" ) . " keyid found\n" if $die_if_empty and not @a; return @a; } sub kf_add { my ( $gl_user, $keyid, $keymaterial ) = @_; # add a new "marked for addition" key for $gl_user. cd_temp_clone(); chdir("keydir"); mkdir("zzz-marked"); _print( "zzz-marked/zzz-marked-for-add-$gl_user$keyid.pub", $keymaterial ); hushed_git( "add", "." ) and die "git add failed\n"; my $fp = fingerprint("zzz-marked/zzz-marked-for-add-$gl_user$keyid.pub"); hushed_git( "commit", "-m", "sskm: add $gl_user$keyid ($fp)" ) and die "git commit failed\n"; system("gitolite push >/dev/null 2>/dev/null") and die "git push failed\n"; } sub kf_confirm_add { my ( $gl_user, $keyid ) = @_; # find entries in both @pubkeys and @marked_for_add whose basename matches $gl_user$keyid my @pk = highlander( $keyid, 0, grep { m(^(.*/)?$gl_user$keyid.pub$) } @pubkeys ); my @mfa = highlander( $keyid, 1, grep { m(^zzz-marked/zzz-marked-for-add-$gl_user$keyid.pub$) } @marked_for_add ); cd_temp_clone(); chdir("keydir"); my $fp = fingerprint( $mfa[0] ); if ( $pk[0] ) { hushed_git( "mv", "-f", $mfa[0], $pk[0] ); hushed_git( "commit", "-m", "sskm: confirm-add (replace) $pk[0] ($fp)" ) and die "git commit failed\n"; } else { hushed_git( "mv", "-f", $mfa[0], "$gl_user$keyid.pub" ); hushed_git( "commit", "-m", "sskm: confirm-add $gl_user$keyid ($fp)" ) and die "git commit failed\n"; } system("gitolite push >/dev/null 2>/dev/null") and die "git push failed\n"; } sub kf_undo_add { # XXX some code at start is shared with kf_confirm_add my ( $gl_user, $keyid ) = @_; my @mfa = highlander( $keyid, 1, grep { m(^zzz-marked/zzz-marked-for-add-$gl_user$keyid.pub$) } @marked_for_add ); cd_temp_clone(); chdir("keydir"); my $fp = fingerprint( $mfa[0] ); hushed_git( "rm", $mfa[0] ); hushed_git( "commit", "-m", "sskm: undo-add $gl_user$keyid ($fp)" ) and die "git commit failed\n"; system("gitolite push >/dev/null 2>/dev/null") and die "git push failed\n"; } sub kf_del { my ( $gl_user, $keyid ) = @_; cd_temp_clone(); chdir("keydir"); mkdir("zzz-marked"); my @pk = highlander( $keyid, 1, grep { m(^(.*/)?$gl_user$keyid.pub$) } @pubkeys ); my $fp = fingerprint( $pk[0] ); hushed_git( "mv", $pk[0], "zzz-marked/zzz-marked-for-del-$gl_user$keyid.pub" ) and die "git mv failed\n"; hushed_git( "commit", "-m", "sskm: del $pk[0] ($fp)" ) and die "git commit failed\n"; system("gitolite push >/dev/null 2>/dev/null") and die "git push failed\n"; } sub kf_confirm_del { my ( $gl_user, $keyid ) = @_; my @mfd = highlander( $keyid, 1, grep { m(^zzz-marked/zzz-marked-for-del-$gl_user$keyid.pub$) } @marked_for_del ); cd_temp_clone(); chdir("keydir"); my $fp = fingerprint( $mfd[0] ); hushed_git( "rm", $mfd[0] ); hushed_git( "commit", "-m", "sskm: confirm-del $gl_user$keyid ($fp)" ) and die "git commit failed\n"; system("gitolite push >/dev/null 2>/dev/null") and die "git push failed\n"; } sub kf_undo_del { my ( $gl_user, $keyid ) = @_; my @mfd = highlander( $keyid, 1, grep { m(^zzz-marked/zzz-marked-for-del-$gl_user$keyid.pub$) } @marked_for_del ); print STDERR " You're undeleting a key that is currently marked for deletion. Hit ENTER to undelete this key Hit Ctrl-C to cancel the undelete Please see documentation for caveats on the undelete process as well as how to actually delete it. "; <>; # yeay... always wanted to do that -- throw away user input! cd_temp_clone(); chdir("keydir"); my $fp = fingerprint( $mfd[0] ); hushed_git( "mv", "-f", $mfd[0], "$gl_user$keyid.pub" ); hushed_git( "commit", "-m", "sskm: undo-del $gl_user$keyid ($fp)" ) and die "git commit failed\n"; system("gitolite push >/dev/null 2>/dev/null") and die "git push failed\n"; } gitolite3-3.6.1/src/commands/sudo000077500000000000000000000012661241446647300167340ustar00rootroot00000000000000#!/bin/sh # Usage: ssh git@host sudo # # Let super-user run commands as any other user. "Super-user" is defined as # "have write access to the gitolite-admin repo". die() { echo "$@" >&2; exit 1; } usage() { perl -lne 'print substr($_, 2) if /^# Usage/../^$/' < $0; exit 1; } [ -z "$2" ] && usage [ "$1" = "-h" ] && usage [ -z "$GL_USER" ] && die GL_USER not set gitolite access -q gitolite-admin $GL_USER W any || die "You are not authorised" user="$1"; shift cmd="$1"; shift # switch user GL_USER="$user" # figure out if the command is allowed from a remote user gitolite query-rc -q COMMANDS $cmd || die "Command '$cmd' not allowed" gitolite $cmd "$@" gitolite3-3.6.1/src/commands/svnserve000077500000000000000000000006031241446647300176270ustar00rootroot00000000000000#!/usr/bin/perl use strict; use warnings; use lib $ENV{GL_LIBDIR}; use Gitolite::Rc; my $svnserve = $rc{SVNSERVE} || ''; $svnserve ||= "/usr/bin/svnserve -r /var/svn/ -t --tunnel-user=%u"; my $cmd = $ENV{SSH_ORIGINAL_COMMAND}; die "expecting 'svnserve -t', got '$cmd'\n" unless $cmd eq 'svnserve -t'; $svnserve =~ s/%u/$ENV{GL_USER}/g; exec $svnserve; die "svnserve exec failed\n"; gitolite3-3.6.1/src/commands/symbolic-ref000077500000000000000000000022171241446647300203520ustar00rootroot00000000000000#!/bin/sh # Usage: ssh git@host symbolic-ref # # allow 'git symbolic-ref' over a gitolite connection # Security: remember all arguments to commands must match a very conservative # pattern. Once that is assured, the symbolic-ref command has no security # related side-effects, so we don't check arguments at all. # Note: because of the restriction on allowed characters in arguments, you # can't supply an arbitrary string to the '-m' option. The simplest # work-around is-to-just-use-join-up-words-like-this if you feel the need to # supply a "reason" string. In any case this is useless by default; you'd # have to have core.logAllRefUpdates set for it to have any meaning. die() { echo "$@" >&2; exit 1; } usage() { perl -lne 'print substr($_, 2) if /^# Usage/../^$/' < $0; exit 1; } [ -z "$1" ] && usage [ "$1" = "-h" ] && usage [ -z "$GL_USER" ] && die GL_USER not set # ---------------------------------------------------------------------- repo=$1; shift repo=${repo%.git} gitolite access -q "$repo" $GL_USER W any || die You are not authorised # change head cd $GL_REPO_BASE/$repo.git git symbolic-ref "$@" gitolite3-3.6.1/src/commands/who-pushed000077500000000000000000000036101241446647300200400ustar00rootroot00000000000000#!/usr/bin/perl use strict; use warnings; use lib $ENV{GL_LIBDIR}; use Gitolite::Easy; =for usage Usage: ssh git@host who-pushed Determine who pushed the given commit. The first few hex digits of the SHA should suffice. Each line of the output contains the following fields: timestamp, a transaction ID, username, refname, and the old and new SHAs for the ref. We assume the logfile names have been left as default, or if changed, in such a way that they come up oldest first when sorted. The program searches ALL the log files, in reverse sorted order (i.e., newest first). This means it could take a long time if your log directory is large and contains lots of old log files. Patches to limit the search to an optional date range are welcome. Note on the "transaction ID" field: if looking at the log file doesn't help you figure out what its purpose is, please just ignore it. =cut usage() if not @ARGV or @ARGV < 2 or $ARGV[0] eq '-h'; usage() if $ARGV[1] !~ /^[0-9a-f]+$/i; my $repo = shift; my $sha = shift; $sha =~ tr/A-F/a-f/; $ENV{GL_USER} and ( can_read($repo) or die "no read permissions on '$repo'" ); # ---------------------------------------------------------------------- my $repodir = "$ENV{GL_REPO_BASE}/$repo.git"; chdir $repodir or die "repo '$repo' missing"; ( my $logdir = $ENV{GL_LOGFILE} ) =~ s(/[^/]+$)(); for my $logfile ( reverse glob("$logdir/*") ) { @ARGV = ($logfile); for my $line ( reverse grep { m(\tupdate\t($repo|$repodir)\t) } <> ) { chomp($line); my @fields = split /\t/, $line; my ( $ts, $pid, $who, $ref, $d_old, $new ) = @fields[ 0, 1, 4, 6, 7, 8 ]; # d_old is what you display my $old = $d_old; $old = "" if $d_old eq ( "0" x 40 ); $old = "$old.." if $old; system("git rev-list $old$new 2>/dev/null | grep ^$sha >/dev/null && echo '$ts $pid $who $ref $d_old $new'"); } } gitolite3-3.6.1/src/commands/writable000077500000000000000000000031741241446647300175730ustar00rootroot00000000000000#!/usr/bin/perl use strict; use warnings; use lib $ENV{GL_LIBDIR}; use Gitolite::Easy; =for usage Usage: gitolite writable |@all on|off|status Disable/re-enable pushes to all repos or named repo. Useful to run non-git-aware backups and so on. 'on' enables, 'off' disables, writes (pushes) to the named repo or all repos. 'status' returns the current status as shell truth (i.e., exit code 0 for writable, 1 for not writable). With 'off', any subsequent text is taken to be the message to be shown to users when their pushes get rejected. If it is not supplied, it will take it from STDIN; this allows longer messages. =cut usage() if not @ARGV or @ARGV < 2 or $ARGV[0] eq '-h'; usage() if $ARGV[1] ne 'on' and $ARGV[1] ne 'off' and $ARGV[1] ne 'status'; my $repo = shift; my $op = shift; # on|off|status if ( $repo eq '@all' ) { _die "you are not authorized" if $ENV{GL_USER} and not is_admin(); } else { _die "you are not authorized" if $ENV{GL_USER} and not( owns($repo) or is_admin() ); } my $msg = join( " ", @ARGV ); # try STDIN only if no msg found in args *and* it's an 'off' command if ( not $msg and $op eq 'off' ) { say2 "...please type the message to be shown to users:"; $msg = join( "", <> ); } my $sf = ".gitolite.down"; my $rb = $ENV{GL_REPO_BASE}; if ( $repo eq '@all' ) { target( $ENV{HOME} ); } else { target("$rb/$repo.git"); } sub target { my $repodir = shift; if ( $op eq 'status' ) { exit 1 if -e "$repodir/$sf"; exit 0; } elsif ( $op eq 'on' ) { unlink "$repodir/$sf"; } elsif ( $op eq 'off' ) { _print( "$repodir/$sf", $msg ); } } gitolite3-3.6.1/src/gitolite000077500000000000000000000060131241446647300157740ustar00rootroot00000000000000#!/usr/bin/perl # all gitolite CLI tools run as sub-commands of this command # ---------------------------------------------------------------------- =for args Usage: gitolite [sub-command] [options] The following built-in subcommands are available; they should all respond to '-h' if you want further details on each: setup 1st run: initial setup; all runs: hook fixups compile compile gitolite.conf query-rc get values of rc variables list-groups list all group names in conf list-users list all users/user groups in conf list-repos list all repos/repo groups in conf list-phy-repos list all repos actually on disk list-memberships list all groups a name is a member of list-members list all members of a group Warnings: - list-users is disk bound and could take a while on sites with 1000s of repos - list-memberships does not check if the name is known; unknown names come back with 2 answers: the name itself and '@all' In addition, running 'gitolite help' should give you a list of custom commands available. They may or may not respond to '-h', depending on how they were written. =cut # ---------------------------------------------------------------------- use FindBin; BEGIN { $ENV{GL_BINDIR} = $FindBin::RealBin; } BEGIN { $ENV{GL_LIBDIR} = "$ENV{GL_BINDIR}/lib"; } use lib $ENV{GL_LIBDIR}; use Gitolite::Rc; use Gitolite::Common; use strict; use warnings; # ---------------------------------------------------------------------- my ( $command, @args ) = @ARGV; gl_log( 'cli', 'gitolite', @ARGV ) if -d $rc{GL_ADMIN_BASE} and $$ == ( $ENV{GL_TID} || 0 ); args(); # the first two commands need options via @ARGV, as they have their own # GetOptions calls and older perls don't have 'GetOptionsFromArray' if ( $command eq 'setup' ) { shift @ARGV; require Gitolite::Setup; Gitolite::Setup->import; setup(); } elsif ( $command eq 'query-rc' ) { shift @ARGV; query_rc(); # doesn't return # the rest don't need @ARGV per se } elsif ( $command eq 'compile' ) { require Gitolite::Conf; Gitolite::Conf->import; compile(@args); } elsif ( $command eq 'trigger' ) { trigger(@args); } elsif ( my $c = _which( "commands/$command", 'x' ) ) { trace( 2, "attempting gitolite command $c" ); _system( $c, @args ); } elsif ( $command eq 'list-phy-repos' ) { _chdir( $rc{GL_REPO_BASE} ); print "$_\n" for ( @{ list_phy_repos(@args) } ); } elsif ( $command =~ /^list-/ ) { trace( 2, "attempting lister command $command" ); require Gitolite::Conf::Load; Gitolite::Conf::Load->import; my $fn = lister_dispatch($command); print "$_\n" for ( @{ $fn->(@args) } ); } else { _die "unknown gitolite sub-command"; } gl_log('END') if $$ == $ENV{GL_TID}; exit 0; sub args { usage() if not $command or $command eq '-h'; } # ---------------------------------------------------------------------- gitolite3-3.6.1/src/gitolite-shell000077500000000000000000000206021241446647300171010ustar00rootroot00000000000000#!/usr/bin/perl # gitolite shell, invoked from ~/.ssh/authorized_keys # ---------------------------------------------------------------------- use FindBin; BEGIN { $ENV{GL_BINDIR} = $FindBin::RealBin; } BEGIN { $ENV{GL_LIBDIR} = "$ENV{GL_BINDIR}/lib"; } use lib $ENV{GL_LIBDIR}; # set HOME BEGIN { $ENV{HOME} = $ENV{GITOLITE_HTTP_HOME} if $ENV{GITOLITE_HTTP_HOME}; } use Gitolite::Rc; use Gitolite::Common; use Gitolite::Conf::Load; use strict; use warnings; # the main() sub expects ssh-ish things; set them up... my $id = ''; if ( exists $ENV{G3T_USER} ) { $id = in_file(); # file:// masquerading as ssh:// for easy testing } elsif ( exists $ENV{SSH_CONNECTION} ) { $id = in_ssh(); } elsif ( exists $ENV{REQUEST_URI} ) { $id = in_http(); } else { _die "who the *heck* are you?"; } # sanity... my $soc = $ENV{SSH_ORIGINAL_COMMAND}; $soc =~ s/[\n\r]+/<>/g; _die "I don't like newlines in the command: '$soc'\n" if $ENV{SSH_ORIGINAL_COMMAND} ne $soc; # the INPUT trigger massages @ARGV and $ENV{SSH_ORIGINAL_COMMAND} as needed trigger('INPUT'); main($id); gl_log('END') if $$ == $ENV{GL_TID}; exit 0; # ---------------------------------------------------------------------- sub in_file { gl_log( 'file', "ARGV=" . join( ",", @ARGV ), "SOC=$ENV{SSH_ORIGINAL_COMMAND}" ); if ( $ENV{SSH_ORIGINAL_COMMAND} =~ /git-\w+-pack/ ) { print STDERR "TRACE: gsh(", join( ")(", @ARGV ), ")\n"; print STDERR "TRACE: gsh(SOC=$ENV{SSH_ORIGINAL_COMMAND})\n"; } return 'file'; } sub in_http { http_setup_die_handler(); _die "GITOLITE_HTTP_HOME not set" unless $ENV{GITOLITE_HTTP_HOME}; _die "fallback to DAV not supported" if $ENV{REQUEST_METHOD} eq 'PROPFIND'; # fake out SSH_ORIGINAL_COMMAND and SSH_CONNECTION when called via http, # so the rest of the code stays the same (except the exec at the end). http_simulate_ssh_connection(); $ENV{REMOTE_USER} ||= $rc{HTTP_ANON_USER}; @ARGV = ( $ENV{REMOTE_USER} ); my $ip; ( $ip = $ENV{SSH_CONNECTION} || '(no-IP)' ) =~ s/ .*//; gl_log( 'http', "ARGV=" . join( ",", @ARGV ), "SOC=" . ( $ENV{SSH_ORIGINAL_COMMAND} || '' ), "FROM=$ip" ); return 'http'; } sub in_ssh { my $ip; ( $ip = $ENV{SSH_CONNECTION} || '(no-IP)' ) =~ s/ .*//; gl_log( 'ssh', "ARGV=" . join( ",", @ARGV ), "SOC=" . ( $ENV{SSH_ORIGINAL_COMMAND} || '' ), "FROM=$ip" ); $ENV{SSH_ORIGINAL_COMMAND} ||= ''; return $ip; } # ---------------------------------------------------------------------- # call this once you are sure arg-1 is the username and SSH_ORIGINAL_COMMAND # has been setup (even if it's not actually coming via ssh). sub main { my $id = shift; # set up the user my $user = $ENV{GL_USER} = shift @ARGV; # set up the repo and the attempted access my ( $verb, $repo ) = parse_soc(); # returns only for git commands sanity($repo); $ENV{GL_REPO} = $repo; my $aa = ( $verb =~ 'upload' ? 'R' : 'W' ); # set up env vars from options set for this repo env_options($repo); # auto-create? if ( repo_missing($repo) and access( $repo, $user, '^C', 'any' ) !~ /DENIED/ ) { require Gitolite::Conf::Store; Gitolite::Conf::Store->import; new_wild_repo( $repo, $user, $aa ); gl_log( 'create', $repo, $user, $aa ); } # a ref of 'any' signifies that this is a pre-git check, where we don't # yet know the ref that will be eventually pushed (and even that won't # apply if it's a read operation). See the matching code in access() for # more information. unless ( $ENV{GL_BYPASS_ACCESS_CHECKS} ) { my $ret = access( $repo, $user, $aa, 'any' ); trigger( 'ACCESS_1', $repo, $user, $aa, 'any', $ret ); _die $ret . "\n(or you mis-spelled the reponame)" if $ret =~ /DENIED/; gl_log( "pre_git", $repo, $user, $aa, 'any', $ret ); } trigger( 'PRE_GIT', $repo, $user, $aa, 'any', $verb ); if ( $ENV{REQUEST_URI} ) { _system( "git", "http-backend" ); } else { my $repodir = "'$rc{GL_REPO_BASE}/$repo.git'"; _system( "git", "shell", "-c", "$verb $repodir" ); } trigger( 'POST_GIT', $repo, $user, $aa, 'any', $verb ); } # ---------------------------------------------------------------------- sub parse_soc { my $soc = $ENV{SSH_ORIGINAL_COMMAND}; $soc ||= 'info'; my $git_commands = "git-upload-pack|git-receive-pack|git-upload-archive"; if ( $soc =~ m(^($git_commands) '/?(.*?)(?:\.git(\d)?)?'$) ) { my ( $verb, $repo, $trace_level ) = ( $1, $2, $3 ); $ENV{D} = $trace_level if $trace_level; _die "invalid repo name: '$repo'" if $repo !~ $REPONAME_PATT; trace( 2, "git command", $soc ); return ( $verb, $repo ); } # after this we should not return; caller expects us to handle it all here # and exit out my @words = split ' ', $soc; if ( $rc{COMMANDS}{ $words[0] } ) { _die "suspicious characters loitering about '$soc'" if $rc{COMMANDS}{ $words[0] } ne 'ua' and $soc !~ $REMOTE_COMMAND_PATT; trace( 2, "gitolite command", $soc ); _system( "gitolite", @words ); exit 0; } _die "unknown git/gitolite command: '$soc'"; } sub sanity { my $repo = shift; _die "'$repo' contains bad characters" if $repo !~ $REPONAME_PATT; _die "'$repo' ends with a '/'" if $repo =~ m(/$); _die "'$repo' contains '..'" if $repo =~ m(\.\.); } # ---------------------------------------------------------------------- # helper functions for "in_http" sub http_setup_die_handler { $SIG{__DIE__} = sub { my $service = ( $ENV{SSH_ORIGINAL_COMMAND} =~ /git-receive-pack/ ? 'git-receive-pack' : 'git-upload-pack' ); my $message = shift; chomp($message); print STDERR "$message\n"; http_print_headers($service); # format the service response, then the message. With initial # help from Ilari and then a more detailed email from Shawn... $service = "# service=$service\n"; $message = "ERR $message\n"; $service = sprintf( "%04X", length($service) + 4 ) . "$service"; # no CRLF on this one $message = sprintf( "%04X", length($message) + 4 ) . "$message"; print $service; print "0000"; # flush-pkt, apparently print $message; print STDERR $service; print STDERR $message; exit 0; # if it's ok for die_webcgi in git.git/http-backend.c, it's ok for me ;-) } } sub http_simulate_ssh_connection { # these patterns indicate normal git usage; see "services[]" in # http-backend.c for how I got that. Also note that "info" is overloaded; # git uses "info/refs...", while gitolite uses "info" or "info?...". So # there's a "/" after info in the list below if ( $ENV{PATH_INFO} =~ m(^/(.*)/(HEAD$|info/refs$|objects/|git-(?:upload|receive)-pack$)) ) { my $repo = $1; my $verb = ( $ENV{REQUEST_URI} =~ /git-receive-pack/ ) ? 'git-receive-pack' : 'git-upload-pack'; $ENV{SSH_ORIGINAL_COMMAND} = "$verb '$repo'"; } else { # this is one of our custom commands; could be anything really, # because of the adc feature my ($verb) = ( $ENV{PATH_INFO} =~ m(^/(\S+)) ); my $args = $ENV{QUERY_STRING}; $args =~ s/\+/ /g; $args =~ s/%([0-9A-Fa-f]{2})/chr(hex($1))/eg; $ENV{SSH_ORIGINAL_COMMAND} = $verb; $ENV{SSH_ORIGINAL_COMMAND} .= " $args" if $args; http_print_headers(); # in preparation for the eventual output! # we also need to pipe STDERR out via STDOUT, else the user doesn't see those messages! open(STDERR, ">&STDOUT") or _die "Can't dup STDOUT: $!"; } $ENV{SSH_CONNECTION} = "$ENV{REMOTE_ADDR} $ENV{REMOTE_PORT} $ENV{SERVER_ADDR} $ENV{SERVER_PORT}"; } my $http_headers_printed = 0; sub http_print_headers { my ( $service, $code, $text ) = @_; return if $http_headers_printed++; $code ||= 200; $text ||= "OK - gitolite"; $|++; print "Status: $code $text\r\n"; print "Expires: Fri, 01 Jan 1980 00:00:00 GMT\r\n"; print "Pragma: no-cache\r\n"; print "Cache-Control: no-cache, max-age=0, must-revalidate\r\n"; if ($service) { print "Content-Type: application/x-$service-advertisement\r\n"; } else { print "Content-Type: text/plain\r\n"; } print "\r\n"; } gitolite3-3.6.1/src/lib/000077500000000000000000000000001241446647300147745ustar00rootroot00000000000000gitolite3-3.6.1/src/lib/Gitolite/000077500000000000000000000000001241446647300165545ustar00rootroot00000000000000gitolite3-3.6.1/src/lib/Gitolite/Cache.pm000066400000000000000000000104201241446647300201120ustar00rootroot00000000000000package Gitolite::Cache; # cache stuff using an external database (redis) # ---------------------------------------------------------------------- @EXPORT = qw( cache_control cache_wrap ); use Exporter 'import'; use Gitolite::Common; use Gitolite::Rc; use Storable qw(freeze thaw); use Redis; my $redis; my $redis_sock = "$ENV{HOME}/.redis-gitolite.sock"; if ( -S $redis_sock ) { _connect_redis(); } else { _start_redis(); _connect_redis(); # this redis db is a transient, caching only, db, so let's not # accidentally use any stale data when if we're just starting up cache_control('stop'); cache_control('start'); } # ---------------------------------------------------------------------- my %wrapped; my $ttl = ( $rc{CACHE_TTL} || ( $rc{GROUPLIST_PGM} ? 900 : 90000 ) ); sub cache_control { my $op = shift; if ( $op eq 'stop' ) { $redis->flushall(); } elsif ( $op eq 'start' ) { $redis->set( 'cache-up', 1 ); } elsif ( $op eq 'flush' ) { flush_repo(@_); } } sub cache_wrap { my $sub = shift; my $tname = $sub; # this is what will show up in the trace output trace( 3, "wrapping '$sub'" ); $sub = ( caller 1 )[0] . "::" . $sub if $sub !~ /::/; return if $wrapped{$sub}++; # in case somehow it gets called twice for the same sub! # collect names of wrapped subs into a redis 'set' $redis->sadd( "SUBWAY", $sub ); # subway? yeah well they wrap subs don't they? my $cref = eval '\&' . $sub; my %opt = @_; # rest of the options come in as a hash. 'list' says this functions # returns a list. 'ttl' is a number to override the default ttl for # the cached value. no strict 'refs'; no warnings 'redefine'; *{$sub} = sub { # the wrapper function my $key = join( ", ", @_ ); trace( 2, "$tname.args", @_ ); if ( cache_up() and defined( my $val = $redis->get("$sub: $key") ) ) { # cache is up and we got a hit, return value from cache if ( $opt{list} ) { trace( 2, "$tname.getl", @{ thaw($val) } ); return @{ thaw($val) }; } else { trace( 2, "$tname.get", $val ); return $val; } } else { # cache is down or we got a miss, compute my ( $r, @r ); if ( $opt{list} ) { @r = $cref->(@_); # provide list context trace( 2, "$tname.setl", @r ); } else { $r = $cref->(@_); # provide scalar context trace( 2, "$tname.set", $r ); } # store computed value in cache if cache is up if ( cache_up() ) { $redis->set( "$sub: $key", ( $opt{list} ? freeze( \@r ) : $r ) ); $redis->expire( "$sub: $key", $opt{ttl} || $ttl ); trace( 2, "$tname.ttl", ( $opt{ttl} || $ttl ) ); } return @r if $opt{list}; return $r; } }; trace( 3, "wrapped '$sub'" ); } sub cache_up { return $redis->exists('cache-up'); } sub flush_repo { my $repo = shift; my @wrapped = $redis->smembers("SUBWAY"); for my $func (@wrapped) { # if we wrap any more functions, make sure they're functions where the # first argument is 'repo' my @keys = $redis->keys("$func: $repo, *"); $redis->del( @keys ) if @keys; } } # ---------------------------------------------------------------------- sub _start_redis { my $conf = join( "", ); $conf =~ s/%HOME/$ENV{HOME}/g; open( REDIS, "|-", "/usr/sbin/redis-server", "-" ) or die "start redis server failed: $!"; print REDIS $conf; close REDIS; # give it a little time to come up select( undef, undef, undef, 0.2 ); } sub _connect_redis { $redis = Redis->new( sock => $redis_sock, encoding => undef ) or die "redis new failed: $!"; $redis->ping or die "redis ping failed: $!"; } 1; __DATA__ # resources maxmemory 50MB port 0 unixsocket %HOME/.redis-gitolite.sock unixsocketperm 700 timeout 0 databases 1 # daemon daemonize yes pidfile %HOME/.redis-gitolite.pid dbfilename %HOME/.redis-gitolite.rdb dir %HOME # feedback loglevel notice logfile %HOME/.redis-gitolite.log # we don't save gitolite3-3.6.1/src/lib/Gitolite/Common.pm000066400000000000000000000226321241446647300203470ustar00rootroot00000000000000package Gitolite::Common; # common (non-gitolite-specific) functions # ---------------------------------------------------------------------- #<<< @EXPORT = qw( print2 dbg _mkdir _open ln_sf tsh_rc sort_u say _warn _chdir _print tsh_text list_phy_repos say2 _die _system slurp tsh_lines trace cleanup_conf_line tsh_try usage tsh_run gen_lfn gl_log dd t_start t_lap ); #>>> use Exporter 'import'; use File::Path qw(mkpath); use Carp qw(carp cluck croak confess); use strict; use warnings; # ---------------------------------------------------------------------- sub print2 { local $/ = "\n"; print STDERR @_; } sub say { local $/ = "\n"; print @_, "\n"; } sub say2 { local $/ = "\n"; print STDERR @_, "\n"; } sub trace { gl_log( "\t" . join( ",", @_[ 1 .. $#_ ] ) ) if $_[0] <= 1 and defined $Gitolite::Rc::rc{LOG_EXTRA}; return unless defined( $ENV{D} ); my $level = shift; return if $ENV{D} < $level; my $sub = ( caller 1 )[3] || ''; $sub =~ s/.*://; if ( not $sub ) { $sub = (caller)[1]; $sub =~ s(.*/(.*))(($1)); } $sub .= ' ' x ( 31 - length($sub) ); say2 "$level\t$sub\t", join( "\t", @_ ); } sub dbg { use Data::Dumper; return unless defined( $ENV{D} ); for my $i (@_) { print STDERR "DBG: " . Dumper($i); } } sub dd { local $ENV{D} = 1; dbg(@_); } { my %start_times; eval "require Time::HiRes"; # we just ignore any errors from this; nothing needs to be done as long as # no code *calls* either of the next two functions. sub t_start { my $name = shift || 'default'; $start_times{$name} = [ Time::HiRes::gettimeofday() ]; } sub t_lap { my $name = shift || 'default'; return Time::HiRes::tv_interval( $start_times{$name} ); } } sub _warn { gl_log( 'warn', @_ ); if ( $ENV{D} and $ENV{D} >= 3 ) { cluck "WARNING: ", @_, "\n"; } elsif ( defined( $ENV{D} ) ) { carp "WARNING: ", @_, "\n"; } else { warn "WARNING: ", @_, "\n"; } } $SIG{__WARN__} = \&_warn; sub _die { gl_log( 'die', @_ ); if ( $ENV{D} and $ENV{D} >= 3 ) { confess "FATAL: " . join( ",", @_ ) . "\n" if defined( $ENV{D} ); } elsif ( defined( $ENV{D} ) ) { croak "FATAL: " . join( ",", @_ ) . "\n"; } else { die "FATAL: " . join( ",", @_ ) . "\n"; } } $SIG{__DIE__} = \&_die; sub usage { _warn(shift) if @_; my $script = (caller)[1]; my $function = ( ( ( caller(1) )[3] ) || ( ( caller(0) )[3] ) ); $function =~ s/.*:://; my $code = slurp($script); $code =~ /^=for $function\b(.*?)^=cut/sm; say2( $1 ? $1 : "...no usage message in $script" ); exit 1; } sub _mkdir { # It's not an error if the directory exists, but it is an error if it # doesn't exist and we can't create it. This includes not guaranteeing # dead symlinks or if mkpath traversal is blocked by a file. my $dir = shift; my $perm = shift; # optional return if -d $dir; mkpath($dir); chmod $perm, $dir if $perm; return 1; } sub _chdir { chdir( $_[0] || $ENV{HOME} ) or _die "chdir $_[0] failed: $!\n"; } sub _system { # run system(), catch errors. Be verbose only if $ENV{D} exists. If not, # exit with if it applies, else just "exit 1". trace( 1, 'system', @_ ); if ( system(@_) != 0 ) { trace( 1, "system() failed", @_, "-> $?" ); if ( $? == -1 ) { die "failed to execute: $!\n" if $ENV{D}; } elsif ( $? & 127 ) { die "child died with signal " . ( $? & 127 ) . "\n" if $ENV{D}; } else { die "child exited with value " . ( $? >> 8 ) . "\n" if $ENV{D}; exit( $? >> 8 ); } exit 1; } } sub _open { open( my $fh, $_[0], $_[1] ) or _die "open $_[1] failed: $!\n"; return $fh; } sub _print { my ( $file, @text ) = @_; my $fh = _open( ">", "$file.$$" ); print $fh @text; close($fh) or _die "close $file failed: $! at ", (caller)[1], " line ", (caller)[2], "\n"; my $oldmode = ( ( stat $file )[2] ); rename "$file.$$", $file; chmod $oldmode, $file if $oldmode; } sub slurp { return unless defined wantarray; local $/ = undef unless wantarray; my $fh = _open( "<", $_[0] ); return <$fh>; } sub dos2unix { # WARNING: when calling this, make sure you supply a list context s/\r\n/\n/g for @_; return @_; } sub ln_sf { trace( 3, @_ ); my ( $srcdir, $glob, $dstdir ) = @_; for my $hook ( glob("$srcdir/$glob") ) { $hook =~ s/$srcdir\///; unlink "$dstdir/$hook"; symlink "$srcdir/$hook", "$dstdir/$hook" or croak "could not symlink $srcdir/$hook to $dstdir\n"; } } sub sort_u { my %uniq; my $listref = shift; return [] unless @{$listref}; undef @uniq{ @{$listref} }; # expect a listref my @sort_u = sort keys %uniq; return \@sort_u; } sub cleanup_conf_line { my $line = shift; return $line if $line =~ /^# \S+ \d+$/; # kill comments, but take care of "#" inside *simple* strings $line =~ s/^((".*?"|[^#"])*)#.*/$1/; # normalise whitespace; keeps later regexes very simple $line =~ s/=/ = /; $line =~ s/\s+/ /g; $line =~ s/^ //; $line =~ s/ $//; return $line; } { my @phy_repos = (); sub list_phy_repos { # use cached value only if it exists *and* no arg was received (i.e., # receiving *any* arg invalidates cache) return \@phy_repos if ( @phy_repos and not @_ ); for my $repo (`find . -name "*.git" -prune`) { chomp($repo); $repo =~ s(\./(.*)\.git$)($1); push @phy_repos, $repo; } trace( 3, scalar(@phy_repos) . " physical repos found" ); return sort_u( \@phy_repos ); } } # generate a timestamp sub gen_ts { my ( $s, $min, $h, $d, $m, $y ) = (localtime)[ 0 .. 5 ]; $y += 1900; $m++; # usual adjustments for ( $s, $min, $h, $d, $m ) { $_ = "0$_" if $_ < 10; } my $ts = "$y-$m-$d.$h:$min:$s"; return $ts; } # generate a log file name sub gen_lfn { my ( $s, $min, $h, $d, $m, $y ) = (localtime)[ 0 .. 5 ]; $y += 1900; $m++; # usual adjustments for ( $s, $min, $h, $d, $m ) { $_ = "0$_" if $_ < 10; } my ($template) = shift; # substitute template parameters and set the logfile name $template =~ s/%y/$y/g; $template =~ s/%m/$m/g; $template =~ s/%d/$d/g; return $template; } my $log_dest; my $syslog_opened = 0; END { closelog() if $syslog_opened; } sub gl_log { # the log filename and the timestamp come from the environment. If we get # called even before they are set, we have no choice but to dump to STDERR # (and probably call "logger"). # tab sep if there's more than one field my $msg = join( "\t", @_ ); $msg =~ s/[\n\r]+/<>/g; my $ts = gen_ts(); my $tid = $ENV{GL_TID} ||= $$; # syslog $log_dest = $Gitolite::Rc::rc{LOG_DEST} || '' if not defined $log_dest; if ($log_dest =~ /syslog/) { # log_dest *includes* syslog if ($syslog_opened == 0) { require Sys::Syslog; Sys::Syslog->import(qw(:standard)); openlog("gitolite" . ( $ENV{GL_TID} ? "[$ENV{GL_TID}]" : "" ), "pid", "local0"); $syslog_opened = 1; } # gl_log is called either directly, or, if the rc variable LOG_EXTRA # is set, from trace(1, ...). The latter use is considered additional # info for troubleshooting. Trace prefixes a tab to the arguments # before calling gl_log, to visually set off such lines in the log # file. Although syslog eats up that leading tab, we use it to decide # the priority/level of the syslog message. syslog( ( $msg =~ /^\t/ ? 'debug' : 'info' ), "%s", $msg); return if $log_dest eq 'syslog'; # log_dest *equals* syslog } my $fh; logger_plus_stderr( "errors found before logging could be setup", "$msg" ) if not $ENV{GL_LOGFILE}; open my $lfh, ">>", $ENV{GL_LOGFILE} or logger_plus_stderr( "errors found but logfile could not be created", "$ENV{GL_LOGFILE}: $!", "$msg" ); print $lfh "$ts\t$tid\t$msg\n"; close $lfh; } sub logger_plus_stderr { open my $fh, "|-", "logger" or confess "it's really not my day is it...?\n"; for (@_) { print STDERR "FATAL: $_\n"; print $fh "FATAL: $_\n"; } exit 1; } # ---------------------------------------------------------------------- # bare-minimum subset of 'Tsh' (see github.com/sitaramc/tsh) { my ( $rc, $text ); sub tsh_rc { return $rc || 0; } sub tsh_text { return $text || ''; } sub tsh_lines { return split /\n/, $text; } sub tsh_try { my $cmd = shift; die "try: expects only one argument" if @_; $text = `( $cmd ) 2>&1; printf RC=\$?`; if ( $text =~ s/RC=(\d+)$// ) { $rc = $1; trace( 3, $text ); return ( not $rc ); } die "couldnt find RC= in result; this should not happen:\n$text\n\n...\n"; } sub tsh_run { open( my $fh, "-|", @_ ) or die "popen failed: $!"; local $/ = undef; $text = <$fh>; close $fh; warn "pclose failed: $!" if $!; $rc = ( $? >> 8 ); trace( 3, $text ); return $text; } } 1; gitolite3-3.6.1/src/lib/Gitolite/Conf.pm000066400000000000000000000052761241446647300200110ustar00rootroot00000000000000package Gitolite::Conf; # explode/parse a conf file # ---------------------------------------------------------------------- @EXPORT = qw( compile explode parse ); use Exporter 'import'; use Gitolite::Rc; use Gitolite::Common; use Gitolite::Conf::Sugar; use Gitolite::Conf::Store; use strict; use warnings; # ---------------------------------------------------------------------- sub compile { _die "'gitolite compile' does not take any arguments" if @_; _chdir( $rc{GL_ADMIN_BASE} ); _chdir("conf"); parse( sugar('gitolite.conf') ); # the order matters; new repos should be created first, to give store a # place to put the individual gl-conf files new_repos(); # cache control if ($rc{CACHE}) { require Gitolite::Cache; Gitolite::Cache->import(qw(cache_control)); cache_control('stop'); } store(); if ($rc{CACHE}) { cache_control('start'); } for my $repo ( @{ $rc{NEW_REPOS_CREATED} } ) { trigger( 'POST_CREATE', $repo ); } } sub parse { my $lines = shift; trace( 3, scalar(@$lines) . " lines incoming" ); my ( $fname, $lnum ); for my $line (@$lines) { ( $fname, $lnum ) = ( $1, $2 ), next if $line =~ /^# (\S+) (\d+)$/; # user or repo groups if ( $line =~ /^(@\S+) = (.*)/ ) { add_to_group( $1, split( ' ', $2 ) ); } elsif ( $line =~ /^repo (.*)/ ) { set_repolist( split( ' ', $1 ) ); } elsif ( $line =~ /^(-|C|R|RW\+?(?:C?D?|D?C?)M?) (.* )?= (.+)/ ) { my $perm = $1; my @refs = parse_refs( $2 || '' ); my @users = parse_users($3); for my $ref (@refs) { for my $user (@users) { add_rule( $perm, $ref, $user, $fname, $lnum ); } } } elsif ( $line =~ /^config (.+) = ?(.*)/ ) { my ( $key, $value ) = ( $1, $2 ); $value =~ s/^['"](.*)["']$/$1/; my @validkeys = split( ' ', ( $rc{GIT_CONFIG_KEYS} || '' ) ); push @validkeys, "gitolite-options\\..*"; my @matched = grep { $key =~ /^$_$/i } @validkeys; _die "git config '$key' not allowed\ncheck GIT_CONFIG_KEYS in the rc file" if ( @matched < 1 ); _die "bad config value '$value'" if $value =~ $UNSAFE_PATT; while ( my ( $mk, $mv ) = each %{ $rc{SAFE_CONFIG} } ) { $value =~ s/%$mk/$mv/g; } add_config( 1, $key, $value ); } elsif ( $line =~ /^subconf (\S+)$/ ) { trace( 3, $line ); set_subconf($1); } else { _warn "syntax error, ignoring: '$line'"; } } parse_done(); } 1; gitolite3-3.6.1/src/lib/Gitolite/Conf/000077500000000000000000000000001241446647300174415ustar00rootroot00000000000000gitolite3-3.6.1/src/lib/Gitolite/Conf/Explode.pm000066400000000000000000000062621241446647300214050ustar00rootroot00000000000000package Gitolite::Conf::Explode; # include/subconf processor # ---------------------------------------------------------------------- @EXPORT = qw( explode ); use Exporter 'import'; use Gitolite::Rc; use Gitolite::Common; use strict; use warnings; # ---------------------------------------------------------------------- # 'seen' for include/subconf files my %included = (); # 'seen' for group names on LHS my %prefixed_groupname = (); sub explode { trace( 3, @_ ); my ( $file, $subconf, $out ) = @_; # seed the 'seen' list if it's empty $included{ device_inode("gitolite.conf") }++ unless %included; my $fh = _open( "<", $file ); while (<$fh>) { my $line = cleanup_conf_line($_); next unless $line =~ /\S/; # subst %HOSTNAME word if rc defines a hostname, else leave as is $line =~ s/%HOSTNAME\b/$rc{HOSTNAME}/g if $rc{HOSTNAME}; $line = prefix_groupnames( $line, $subconf ) if $subconf ne 'master'; if ( $line =~ /^(include|subconf) (?:(\S+) )?(\S.+)$/ ) { incsub( $1, $2, $3, $subconf, $out ); } else { # normal line, send it to the callback function push @{$out}, "# $file $."; push @{$out}, $line; } } } sub incsub { my $is_subconf = ( +shift eq 'subconf' ); my ( $new_subconf, $include_glob, $current_subconf, $out ) = @_; _die "subconf '$current_subconf' attempting to run 'subconf'\n" if $is_subconf and $current_subconf ne 'master'; _die "invalid include/subconf file/glob '$include_glob'" unless $include_glob =~ /^"(.+)"$/ or $include_glob =~ /^'(.+)'$/; $include_glob = $1; trace( 3, $is_subconf, $include_glob ); for my $file ( glob($include_glob) ) { _warn("included file not found: '$file'"), next unless -f $file; _die "invalid include/subconf filename '$file'" unless $file =~ m(([^/]+).conf$); my $basename = $1; next if already_included($file); if ($is_subconf) { push @{$out}, "subconf " . ( $new_subconf || $basename ); explode( $file, ( $new_subconf || $basename ), $out ); push @{$out}, "subconf $current_subconf"; } else { explode( $file, $current_subconf, $out ); } } } sub prefix_groupnames { my ( $line, $subconf ) = @_; my $lhs = ''; # save 'foo' if it's an '@foo = list' line $lhs = $1 if $line =~ /^@(\S+) = /; # prefix all @groups in the line $line =~ s/(^| )(@\S+)(?= |$)/ $1 . ($prefixed_groupname{$subconf}{$2} || $2) /ge; # now prefix the LHS and store it if needed if ($lhs) { $line =~ s/^@\S+ = /"\@$subconf.$lhs = "/e; $prefixed_groupname{$subconf}{"\@$lhs"} = "\@$subconf.$lhs"; trace( 3, "prefixed_groupname.$subconf.\@$lhs = \@$subconf.$lhs" ); } return $line; } sub already_included { my $file = shift; my $file_id = device_inode($file); return 0 unless $included{$file_id}++; _warn("$file already included"); trace( 3, "$file already included" ); return 1; } sub device_inode { my $file = shift; trace( 3, $file, ( stat $file )[ 0, 1 ] ); return join( "/", ( stat $file )[ 0, 1 ] ); } 1; gitolite3-3.6.1/src/lib/Gitolite/Conf/Load.pm000066400000000000000000000437151241446647300206700ustar00rootroot00000000000000package Gitolite::Conf::Load; # load conf data from stored files # ---------------------------------------------------------------------- @EXPORT = qw( load access git_config env_options option repo_missing creator vrefs lister_dispatch ); use Exporter 'import'; use Cwd; use Gitolite::Rc; use Gitolite::Common; use strict; use warnings; # ---------------------------------------------------------------------- # our variables, because they get loaded by a 'do' our $data_version = ''; our %repos; our %one_repo; our %groups; our %patterns; our %configs; our %one_config; our %split_conf; my $subconf = 'master'; my %listers = ( 'list-groups' => \&list_groups, 'list-users' => \&list_users, 'list-repos' => \&list_repos, 'list-memberships' => \&list_memberships, 'list-members' => \&list_members, ); # helps maintain the "cache" in both "load_common" and "load_1" my $last_repo = ''; # ---------------------------------------------------------------------- { my $loaded_repo = ''; sub load { my $repo = shift or _die "load() needs a reponame"; trace( 3, "$repo" ); if ( $repo ne $loaded_repo ) { load_common(); load_1($repo); $loaded_repo = $repo; } } } sub access { my ( $repo, $user, $aa, $ref ) = @_; trace( 2, $repo, $user, $aa, $ref ); _die "invalid user '$user'" if not( $user and $user =~ $USERNAME_PATT ); sanity($repo); my @rules; my $deny_rules; load($repo); @rules = rules( $repo, $user ); $deny_rules = option( $repo, 'deny-rules' ); # sanity check the only piece the user can control _die "invalid characters in ref or filename: '$ref'\n" unless $ref =~ m(^VREF/NAME/) or $ref =~ $REF_OR_FILENAME_PATT; # apparently we can't always force sanity; at least what we *return* # should be sane/safe. This pattern is based on REF_OR_FILENAME_PATT. ( my $safe_ref = $ref ) =~ s([^-0-9a-zA-Z._\@/+ :,])(.)g; trace( 3, "safe_ref", $safe_ref ) if $ref ne $safe_ref; # when a real repo doesn't exist, ^C is a pre-requisite for any other # check to give valid results. if ( $aa ne '^C' and $repo !~ /^\@/ and $repo =~ $REPONAME_PATT and repo_missing($repo) ) { my $iret = access( $repo, $user, '^C', $ref ); $iret =~ s/\^C/$aa/; return $iret if $iret =~ /DENIED/; } # similarly, ^C must be denied if the repo exists if ( $aa eq '^C' and not repo_missing($repo) ) { trace( 2, "DENIED by existence" ); return "$aa $safe_ref $repo $user DENIED by existence"; } trace( 3, scalar(@rules) . " rules found" ); $rc{RULE_TRACE} = ''; for my $r (@rules) { $rc{RULE_TRACE} .= " " . $r->[0] . " "; my $perm = $r->[1]; my $refex = $r->[2]; $refex =~ s(/USER/)(/$user/); trace( 3, "perm=$perm, refex=$refex" ); $rc{RULE_TRACE} .= "d"; # skip 'deny' rules if the ref is not (yet) known next if $perm eq '-' and $ref eq 'any' and not $deny_rules; $rc{RULE_TRACE} .= "r"; # rule matches if ref matches or ref is any (see gitolite-shell) next unless $ref =~ /^$refex/ or $ref eq 'any'; $rc{RULE_TRACE} .= "D"; trace( 2, "DENIED by $refex" ) if $perm eq '-'; return "$aa $safe_ref $repo $user DENIED by $refex" if $perm eq '-'; # $perm can be RW\+?(C|D|CD|DC)?M?. $aa can be W, +, C or D, or # any of these followed by "M". ( my $aaq = $aa ) =~ s/\+/\\+/; $aaq =~ s/M/.*M/; $rc{RULE_TRACE} .= "A"; # as far as *this* ref is concerned we're ok return $refex if ( $perm =~ /$aaq/ ); $rc{RULE_TRACE} .= "p"; } $rc{RULE_TRACE} .= " F"; trace( 2, "DENIED by fallthru" ); return "$aa $safe_ref $repo $user DENIED by fallthru"; } # cache control if ($rc{CACHE}) { require Gitolite::Cache; Gitolite::Cache::cache_wrap('Gitolite::Conf::Load::access'); } sub git_config { my ( $repo, $key, $empty_values_OK ) = @_; $key ||= '.'; if ( repo_missing($repo) ) { load_common(); } else { load($repo); } # read comments bottom up my %ret = # and take the second and third elements to make up your new hash map { $_->[1] => $_->[2] } # keep only the ones where the second element matches your key grep { $_->[1] =~ qr($key) } # sort this list of listrefs by the first element in each list ref'd to sort { $a->[0] <=> $b->[0] } # dereference it (into a list of listrefs) map { @$_ } # take the value of that entry map { $configs{$_} } # if it has an entry in %configs grep { $configs{$_} } # for each "repo" that represents us memberships( 'repo', $repo ); # %configs looks like this (for each 'foo' that is in memberships()) # 'foo' => [ [ 6, 'foo.bar', 'repo' ], [ 7, 'foodbar', 'repoD' ], [ 8, 'foo.czar', 'jule' ] ], # the first map gets you the value # [ [ 6, 'foo.bar', 'repo' ], [ 7, 'foodbar', 'repoD' ], [ 8, 'foo.czar', 'jule' ] ], # the deref gets you # [ 6, 'foo.bar', 'repo' ], [ 7, 'foodbar', 'repoD' ], [ 8, 'foo.czar', 'jule' ] # the sort rearranges it (in this case it's already sorted but anyway...) # the grep gets you this, assuming the key is foo.bar (and "." is regex ".') # [ 6, 'foo.bar', 'repo' ], [ 7, 'foodbar', 'repoD' ] # and the final map does this: # 'foo.bar'=>'repo' , 'foodbar'=>'repoD' # now some of these will have an empty key; we need to delete them unless # we're told empty values are OK unless ($empty_values_OK) { my ( $k, $v ); while ( ( $k, $v ) = each %ret ) { delete $ret{$k} if not $v; } } my ( $k, $v ); my $creator = creator($repo); while ( ( $k, $v ) = each %ret ) { $v =~ s/%GL_REPO/$repo/g; $v =~ s/%GL_CREATOR/$creator/g if $creator; $ret{$k} = $v; } map { trace( 3, "$_", "$ret{$_}" ) } ( sort keys %ret ) if $ENV{D}; return \%ret; } sub env_options { return unless -f "$rc{GL_ADMIN_BASE}/conf/gitolite.conf-compiled.pm"; # prevent catch-22 during initial install my $cwd = getcwd(); my $repo = shift; map { delete $ENV{$_} } grep { /^GL_OPTION_/ } keys %ENV; my $h = git_config( $repo, '^gitolite-options.ENV\.' ); while ( my ( $k, $v ) = each %$h ) { next unless $k =~ /^gitolite-options.ENV\.(\w+)$/; $ENV{ "GL_OPTION_" . $1 } = $v; } chdir($cwd); } sub option { my ( $repo, $option ) = @_; $option = "gitolite-options.$option"; my $ret = git_config( $repo, "^\Q$option\E\$" ); return '' unless %$ret; return $ret->{$option}; } sub sanity { my $repo = shift; _die "invalid repo '$repo'" if not( $repo and $repo =~ $REPOPATT_PATT ); _die "'$repo' ends with a '/'" if $repo =~ m(/$); _die "'$repo' contains '..'" if $repo =~ $REPONAME_PATT and $repo =~ m(\.\.); } sub repo_missing { my $repo = shift; sanity($repo); return not -d "$rc{GL_REPO_BASE}/$repo.git"; } # ---------------------------------------------------------------------- sub load_common { _chdir( $rc{GL_ADMIN_BASE} ); # we take an unusual approach to caching this function! # (requires that first call to load_common is before first call to load_1) if ( $last_repo and $split_conf{$last_repo} ) { delete $repos{$last_repo}; delete $configs{$last_repo}; return; } my $cc = "conf/gitolite.conf-compiled.pm"; _die "parse '$cc' failed: " . ( $! or $@ ) unless do $cc; if ( data_version_mismatch() ) { _system("gitolite setup"); _die "parse '$cc' failed: " . ( $! or $@ ) unless do $cc; _die "data version update failed; this is serious" if data_version_mismatch(); } } sub load_1 { my $repo = shift; return if $repo =~ /^\@/; trace( 3, $repo ); if ( repo_missing($repo) ) { trace( 1, "repo '$repo' missing" ) if $repo =~ $REPONAME_PATT; return; } _chdir("$rc{GL_REPO_BASE}/$repo.git"); if ( $repo eq $last_repo ) { $repos{$repo} = $one_repo{$repo}; $configs{$repo} = $one_config{$repo} if $one_config{$repo}; return; } if ( -f "gl-conf" ) { return if not $split_conf{$repo}; my $cc = "./gl-conf"; _die "parse '$cc' failed: " . ( $! or $@ ) unless do $cc; $last_repo = $repo; $repos{$repo} = $one_repo{$repo}; $configs{$repo} = $one_config{$repo} if $one_config{$repo}; } else { _die "split conf set, gl-conf not present for '$repo'" if $split_conf{$repo}; } } { my $lastrepo = ''; my $lastuser = ''; my @cached = (); sub rules { my ( $repo, $user ) = @_; trace( 3, $repo, $user ); return @cached if ( $lastrepo eq $repo and $lastuser eq $user and @cached ); my @rules = (); my @repos = memberships( 'repo', $repo ); my @users = memberships( 'user', $user, $repo ); trace( 3, "memberships: " . scalar(@repos) . " repos and " . scalar(@users) . " users found" ); for my $r (@repos) { for my $u (@users) { push @rules, @{ $repos{$r}{$u} } if exists $repos{$r} and exists $repos{$r}{$u}; } } @rules = sort { $a->[0] <=> $b->[0] } @rules; $lastrepo = $repo; $lastuser = $user; @cached = @rules; # however if the repo was missing, invalidate the cache $lastrepo = '' if repo_missing($repo); return @rules; } sub vrefs { my ( $repo, $user ) = @_; # fill the cache if needed rules( $repo, $user ) unless ( $lastrepo eq $repo and $lastuser eq $user and @cached ); my %seen; my @vrefs = grep { /^VREF\// and not $seen{$_}++ } map { $_->[2] } @cached; return @vrefs; } } sub memberships { trace( 3, @_ ); my ( $type, $base, $repo ) = @_; $repo ||= ''; my @ret; my $base2 = ''; @ret = ( $base, '@all' ); if ( $type eq 'repo' ) { # first, if a repo, say, pub/sitaram/project, has a gl-creator file # that says "sitaram", find memberships for pub/CREATOR/project also $base2 = generic_name($base); # second, you need to check in %repos also for my $i ( keys %repos, keys %configs ) { if ( $base eq $i or $base =~ /^$i$/ or $base2 and ( $base2 eq $i or $base2 =~ /^$i$/ ) ) { push @ret, $i; } } } push @ret, @{ $groups{$base} } if exists $groups{$base}; push @ret, @{ $groups{$base2} } if $base2 and exists $groups{$base2}; for my $i ( keys %{ $patterns{groups} } ) { if ( $base =~ /^$i$/ or $base2 and ( $base2 =~ /^$i$/ ) ) { push @ret, @{ $groups{$i} }; } } push @ret, @{ ext_grouplist($base) } if $type eq 'user' and $rc{GROUPLIST_PGM}; if ( $type eq 'user' and $repo and not repo_missing($repo) ) { # find the roles this user has when accessing this repo and add those # in as groupnames he is a member of. You need the already existing # memberships for this; see below this function for an example push @ret, user_roles( $base, $repo, @ret ); } @ret = @{ sort_u( \@ret ) }; trace( 3, sort @ret ); return @ret; } =for example conf/gitolite.conf: @g1 = u1 @g2 = u1 # now user is a member of both g1 and g2 gl-perms for repo being accessed: READERS @g1 This should result in @READERS being added to the memberships that u1 has (when accessing this repo). So we send the current list (@g1, @g2) to user_roles(), otherwise it has to redo that logic. =cut sub data_version_mismatch { return $data_version ne glrc('current-data-version'); } sub user_roles { my ( $user, $repo, @eg ) = @_; # eg == existing groups (that user is already known to be a member of) my %eg = map { $_ => 1 } @eg; my %ret = (); my $f = "$rc{GL_REPO_BASE}/$repo.git/gl-perms"; my @roles = (); if ( -f $f ) { my $fh = _open( "<", $f ); chomp( @roles = <$fh> ); } push @roles, "CREATOR = " . creator($repo); for (@roles) { # READERS u3 u4 @g1 s/^\s+//; s/ +$//; s/=/ /; s/\s+/ /g; s/^\@//; next if /^#/; next unless /\S/; my ( $role, @members ) = split; # role = READERS, members = u3, u4, @g1 if ( $role ne 'CREATOR' and not $rc{ROLES}{$role} ) { _warn "role '$role' not allowed, ignoring"; next; } for my $m (@members) { if ( $m !~ $USERNAME_PATT ) { _warn "ignoring '$m' in perms line"; next; } # if user eq u3/u4, or is a member of @g1, he has role READERS $ret{ '@' . $role } = 1 if $m eq $user or $eg{$m}; } } return keys %ret; } sub generic_name { my $base = shift; my $base2 = ''; my $creator; # get the creator name. For not-yet-born repos this is $ENV{GL_USER}, # which should be set in all cases that we care about, viz., where we are # checking ^C permissions before new_wild_repo(), and the info command. # In particular, 'gitolite access' can't be used to check ^C perms on wild # repos that contain "CREATOR" if GL_USER is not set. $creator = creator($base); $base2 = $base; $base2 =~ s(\b$creator\b)(CREATOR) if $creator; $base2 = '' if $base2 eq $base; # if there was no change return $base2; } sub creator { my $repo = shift; sanity($repo); return ( $ENV{GL_USER} || '' ) if repo_missing($repo); my $f = "$rc{GL_REPO_BASE}/$repo.git/gl-creator"; my $creator = ''; chomp( $creator = slurp($f) ) if -f $f; return $creator; } { my %cache = (); sub ext_grouplist { my $user = shift; my $pgm = $rc{GROUPLIST_PGM}; return [] if not $pgm; return $cache{$user} if $cache{$user}; my @extgroups = map { s/^@?/@/; $_; } split ' ', `$rc{GROUPLIST_PGM} $user`; return ( $cache{$user} = \@extgroups ); } } # ---------------------------------------------------------------------- # api functions # ---------------------------------------------------------------------- sub lister_dispatch { my $command = shift; my $fn = $listers{$command} or _die "unknown gitolite sub-command"; return $fn; } =for list_groups Usage: gitolite list-groups - lists all group names in conf - no options, no flags =cut sub list_groups { usage() if @_; load_common(); my @g = (); while ( my ( $k, $v ) = each(%groups) ) { push @g, @{$v}; } return ( sort_u( \@g ) ); } =for list_users Usage: gitolite list-users [] List all users and groups explicitly named in a rule. User names not mentioned in an access rule will not show up; you have to run 'list-members' on each group name yourself to see them. WARNING: may be slow if you have thousands of repos. The optional repo name pattern is an unanchored regex; it can speed things up if you're interested only in users of a matching set of repos. This is only an optimisation, not an actual access list; you will still have to pipe it to 'gitolite access' with appropriate arguments to get an actual access list. =cut sub list_users { my $patt = shift || '.'; usage() if $patt eq '-h' or @_; my $count = 0; my $total = 0; load_common(); my @u = map { keys %{$_} } values %repos; $total = scalar( grep { /$patt/ } keys %split_conf ); warn "WARNING: you have $total repos to check; this could take some time!\n" if $total > 100; for my $one ( grep { /$patt/ } keys %split_conf ) { load_1($one); $count++; print STDERR "$count / $total\r" if not( $count % 100 ) and timer(5); push @u, map { keys %{$_} } values %one_repo; } print STDERR "\n" if $count >= 100; return ( sort_u( \@u ) ); } =for list_repos Usage: gitolite list-repos - lists all repos/repo groups in conf - no options, no flags =cut sub list_repos { usage() if @_; load_common(); my @r = keys %repos; push @r, keys %split_conf; return ( sort_u( \@r ) ); } =for list_memberships Usage: gitolite list-memberships -u|-r List all groups a name is a member of. One of the flags '-u' or '-r' is mandatory, to specify if the name is a user or a repo. For users, the output includes the result from GROUPLIST_PGM, if it is defined. For repos, the output includes any repo patterns that the repo name matches, as well as any groups that contain those patterns. =cut sub list_memberships { require Getopt::Long; my ( $user, $repo, $help ); Getopt::Long::GetOptionsFromArray( \@_, 'user|u=s' => \$user, 'repo|r=s' => \$repo, 'help|h' => \$help, ); usage() if $help or ( not $user and not $repo ); load_common(); my @m; if ( $user and $repo ) { # unsupported/undocumented except via "in_role()" in Easy.pm @m = memberships( 'user', $user, $repo ); } elsif ($user) { @m = memberships( 'user', $user ); } elsif ($repo) { @m = memberships( 'repo', $repo ); } @m = grep { $_ ne '@all' and $_ ne ( $user || $repo ) } @m; return ( sort_u( \@m ) ); } =for list_members Usage: gitolite list-members - list all members of a group - takes one group name =cut sub list_members { usage() if @_ and $_[0] eq '-h' or not @_; my $name = shift; load_common(); my @m = (); while ( my ( $k, $v ) = each(%groups) ) { for my $g ( @{$v} ) { push @m, $k if $g eq $name; } } return ( sort_u( \@m ) ); } # ---------------------------------------------------------------------- { my $start_time = 0; sub timer { unless ($start_time) { $start_time = time(); return 0; } my $elapsed = shift; return 0 if time() - $start_time < $elapsed; $start_time = time(); return 1; } } 1; gitolite3-3.6.1/src/lib/Gitolite/Conf/Store.pm000066400000000000000000000260521241446647300211000ustar00rootroot00000000000000package Gitolite::Conf::Store; # receive parsed conf data and store it # ---------------------------------------------------------------------- @EXPORT = qw( add_to_group set_repolist parse_refs parse_users add_rule add_config set_subconf expand_list new_repos new_repo new_wild_repo hook_repos store parse_done ); use Exporter 'import'; use Data::Dumper; $Data::Dumper::Indent = 1; $Data::Dumper::Sortkeys = 1; use Gitolite::Rc; use Gitolite::Common; use Gitolite::Hooks::Update; use Gitolite::Hooks::PostUpdate; use strict; use warnings; # ---------------------------------------------------------------------- my %repos; my %groups; my %configs; my %split_conf; my @repolist; # current repo list; reset on each 'repo ...' line my $subconf = 'master'; my $nextseq = 0; my %ignored; # ---------------------------------------------------------------------- sub add_to_group { my ( $lhs, @rhs ) = @_; _die "bad group '$lhs'" unless $lhs =~ $REPONAME_PATT; map { _die "bad expansion '$_'" unless $_ =~ $REPOPATT_PATT } @rhs; # store the group association, but overload it to keep track of when # the group was *first* created by using $subconf as the *value* do { $groups{$lhs}{$_} ||= $subconf } for ( expand_list(@rhs) ); # create the group hash even if empty $groups{$lhs} = {} unless $groups{$lhs}; } sub set_repolist { my @in = @_; @repolist = (); # ...sanity checks while (@in) { $_ = shift @in; if ( check_subconf_repo_disallowed( $subconf, $_ ) ) { if ( exists $groups{$_} ) { # groupname disallowed; try individual members now ( my $g = $_ ) =~ s/^\@$subconf\./\@/; _warn "expanding '$g'; this *may* slow down compilation"; unshift @in, keys %{ $groups{$_} }; next; } $ignored{$subconf}{$_} = 1; next; } _warn "explicit '.git' extension ignored for $_.git" if s/\.git$//; _die "bad reponame '$_'" if $_ !~ $REPOPATT_PATT; push @repolist, $_; } } sub parse_refs { my $refs = shift; my @refs; @refs = split( ' ', $refs ) if $refs; @refs = expand_list(@refs); # if no ref is given, this PERM applies to all refs @refs = qw(refs/.*) unless @refs; # fully qualify refs that dont start with "refs/" or "VREF/"; # prefix them with "refs/heads/" @refs = map { m(^(refs|VREF)/) or s(^)(refs/heads/); $_ } @refs; return @refs; } sub parse_users { my $users = shift; my @users = split ' ', $users; do { _die "bad username '$_'" unless $_ =~ $USERNAME_PATT } for @users; return @users; } sub add_rule { my ( $perm, $ref, $user, $fname, $lnum ) = @_; _warn "doesn't make sense to supply a ref ('$ref') for 'R' rule" if $perm eq 'R' and $ref ne 'refs/.*'; _warn "possible undeclared group '$user'" if $user =~ /^@/ and not $groups{$user} and not $rc{GROUPLIST_PGM} and not special_group($user); _die "bad ref '$ref'" unless $ref =~ $REPOPATT_PATT; _die "bad user '$user'" unless $user =~ $USERNAME_PATT; $nextseq++; store_rule_info( $nextseq, $fname, $lnum ); for my $repo (@repolist) { push @{ $repos{$repo}{$user} }, [ $nextseq, $perm, $ref ]; } sub special_group { # ok perl doesn't really have lexical subs (at least not the older # perls I want to support) but let's pretend... my $g = shift; $g =~ s/^\@//; return 1 if $g eq 'all' or $g eq 'CREATOR'; return 1 if $rc{ROLES}{$g}; return 0; } } sub add_config { my ( $n, $key, $value ) = @_; $nextseq++; for my $repo (@repolist) { push @{ $configs{$repo} }, [ $nextseq, $key, $value ]; } } sub set_subconf { $subconf = shift; _die "bad subconf '$subconf'" unless $subconf =~ /^[-\w.]+$/; } # ---------------------------------------------------------------------- sub expand_list { my @list = @_; my @new_list = (); for my $item (@list) { if ( $item =~ /^@/ and $item ne '@all' ) # nested group { _die "undefined group '$item'" unless $groups{$item}; # add those names to the list push @new_list, sort keys %{ $groups{$item} }; } else { push @new_list, $item; } } return @new_list; } sub new_repos { trace(3); _chdir( $rc{GL_REPO_BASE} ); # normal repos my @repos = grep { $_ =~ $REPONAME_PATT and not /^@/ } ( sort keys %repos, sort keys %configs ); # add in members of repo groups map { push @repos, keys %{ $groups{$_} } } grep { /^@/ and $_ ne '@all' } keys %repos; for my $repo ( @{ sort_u( \@repos ) } ) { next unless $repo =~ $REPONAME_PATT; # skip repo patterns next if $repo =~ m(^\@|EXTCMD/); # skip groups and fake repos # use gl-conf as a sentinel hook_1($repo) if -d "$repo.git" and not -f "$repo.git/gl-conf"; if ( not -d "$repo.git" ) { push @{ $rc{NEW_REPOS_CREATED} }, $repo; trigger( 'PRE_CREATE', $repo ); new_repo($repo); } } } sub new_repo { my $repo = shift; trace( 3, $repo ); _mkdir("$repo.git"); _chdir("$repo.git"); _system("git init --bare >&2"); _chdir( $rc{GL_REPO_BASE} ); hook_1($repo); } sub new_wild_repo { my ( $repo, $user, $aa ) = @_; _chdir( $rc{GL_REPO_BASE} ); trigger( 'PRE_CREATE', $repo, $user, $aa ); new_repo($repo); _print( "$repo.git/gl-creator", $user ); trigger( 'POST_CREATE', $repo, $user, $aa ); _chdir( $rc{GL_ADMIN_BASE} ); } sub hook_repos { trace(3); # all repos, all hooks _chdir( $rc{GL_REPO_BASE} ); for my $repo (`find . -name "*.git" -prune`) { chomp($repo); $repo =~ s/\.git$//; $repo =~ s(^\./)(); hook_1($repo); } } sub store { trace(3); # first write out the ones for the physical repos _chdir( $rc{GL_REPO_BASE} ); my $phy_repos = list_phy_repos(1); for my $repo ( @{$phy_repos} ) { store_1($repo); } _chdir( $rc{GL_ADMIN_BASE} ); store_common(); } sub parse_done { for my $ig ( sort keys %ignored ) { _warn "subconf '$ig' attempting to set access for " . join( ", ", sort keys %{ $ignored{$ig} } ); } close_rule_info(); } # ---------------------------------------------------------------------- sub check_subconf_repo_disallowed { # trying to set access for $repo (='foo')... my ( $subconf, $repo ) = @_; trace( 2, $subconf, $repo ); # processing the master config, not a subconf return 0 if $subconf eq 'master'; # subconf is also called 'foo' (you're allowed to have a # subconf that is only concerned with one repo) return 0 if $subconf eq $repo; # same thing in big-config-land; foo is just @foo now return 0 if ( "\@$subconf" eq $repo ); my @matched = grep { $repo =~ /^$_$/ } grep { $groups{"\@$subconf"}{$_} eq 'master' } sort keys %{ $groups{"\@$subconf"} }; return 0 if @matched > 0; trace( 2, "-> disallowed" ); return 1; } sub store_1 { # warning: writes and *deletes* it from %repos and %configs my ($repo) = shift; trace( 3, $repo ); return unless ( $repos{$repo} or $configs{$repo} ) and -d "$repo.git"; my ( %one_repo, %one_config ); open( my $compiled_fh, ">", "$repo.git/gl-conf" ) or return; my $dumped_data = ''; if ( $repos{$repo} ) { $one_repo{$repo} = $repos{$repo}; delete $repos{$repo}; $dumped_data = Data::Dumper->Dump( [ \%one_repo ], [qw(*one_repo)] ); } if ( $configs{$repo} ) { $one_config{$repo} = $configs{$repo}; delete $configs{$repo}; $dumped_data .= Data::Dumper->Dump( [ \%one_config ], [qw(*one_config)] ); } print $compiled_fh $dumped_data; close $compiled_fh; $split_conf{$repo} = 1; } sub store_common { trace(3); my $cc = "conf/gitolite.conf-compiled.pm"; my $compiled_fh = _open( ">", "$cc.new" ); my %patterns = (); my $data_version = glrc('current-data-version'); trace( 3, "data_version = $data_version" ); print $compiled_fh Data::Dumper->Dump( [$data_version], [qw(*data_version)] ); my $dumped_data = Data::Dumper->Dump( [ \%repos ], [qw(*repos)] ); $dumped_data .= Data::Dumper->Dump( [ \%configs ], [qw(*configs)] ) if %configs; print $compiled_fh $dumped_data; if (%groups) { my %groups = %{ inside_out( \%groups ) }; $dumped_data = Data::Dumper->Dump( [ \%groups ], [qw(*groups)] ); print $compiled_fh $dumped_data; # save patterns in %groups for faster handling of multiple repos, such # as happens in the various POST_COMPILE scripts for my $k ( keys %groups ) { $patterns{groups}{$k} = 1 unless $k =~ $REPONAME_PATT; } } print $compiled_fh Data::Dumper->Dump( [ \%patterns ], [qw(*patterns)] ) if %patterns; print $compiled_fh Data::Dumper->Dump( [ \%split_conf ], [qw(*split_conf)] ) if %split_conf; close $compiled_fh or _die "close compiled-conf failed: $!\n"; rename "$cc.new", $cc; } { my $hook_reset = 0; sub hook_1 { my $repo = shift; trace( 3, $repo ); # reset the gitolite supplied hooks, in case someone fiddled with # them, but only once per run if ( not $hook_reset ) { _mkdir("$rc{GL_ADMIN_BASE}/hooks/common"); _mkdir("$rc{GL_ADMIN_BASE}/hooks/gitolite-admin"); _print( "$rc{GL_ADMIN_BASE}/hooks/common/update", update_hook() ); _print( "$rc{GL_ADMIN_BASE}/hooks/gitolite-admin/post-update", post_update_hook() ); chmod 0755, "$rc{GL_ADMIN_BASE}/hooks/common/update"; chmod 0755, "$rc{GL_ADMIN_BASE}/hooks/gitolite-admin/post-update"; $hook_reset++; } # propagate user-defined (custom) hooks to all repos ln_sf( "$rc{LOCAL_CODE}/hooks/common", "*", "$repo.git/hooks" ) if $rc{LOCAL_CODE}; # override/propagate gitolite defined hooks for all repos ln_sf( "$rc{GL_ADMIN_BASE}/hooks/common", "*", "$repo.git/hooks" ); # override/propagate gitolite defined hooks for the admin repo ln_sf( "$rc{GL_ADMIN_BASE}/hooks/gitolite-admin", "*", "$repo.git/hooks" ) if $repo eq 'gitolite-admin'; } } sub inside_out { my $href = shift; # input conf: @aa = bb cc @bb = @aa dd my %ret = (); while ( my ( $k, $v ) = each( %{$href} ) ) { # $k is '@aa', $v is a href for my $k2 ( keys %{$v} ) { # $k2 is bb, then cc push @{ $ret{$k2} }, $k; } } return \%ret; # %groups = ( 'bb' => [ '@bb', '@aa' ], 'cc' => [ '@bb', '@aa' ], 'dd' => [ '@bb' ]); } { my $ri_fh = ''; sub store_rule_info { $ri_fh = _open( ">", $rc{GL_ADMIN_BASE} . "/conf/rule_info" ) unless $ri_fh; # $nextseq, $fname, $lnum print $ri_fh join( "\t", @_ ) . "\n"; } sub close_rule_info { close $ri_fh or die "close rule_info file failed: $!"; } } 1; gitolite3-3.6.1/src/lib/Gitolite/Conf/Sugar.pm000066400000000000000000000112451241446647300210630ustar00rootroot00000000000000# and now for something completely different... package SugarBox; sub run_sugar_script { my ( $ss, $lref ) = @_; do $ss if -r $ss; $lref = sugar_script($lref); return $lref; } # ---------------------------------------------------------------------- package Gitolite::Conf::Sugar; # syntactic sugar for the conf file, including site-local macros # ---------------------------------------------------------------------- @EXPORT = qw( sugar ); use Exporter 'import'; use Gitolite::Rc; use Gitolite::Common; use Gitolite::Conf::Explode; use strict; use warnings; # ---------------------------------------------------------------------- sub sugar { # gets a filename, returns a listref my @lines = (); explode( shift, 'master', \@lines ); my $lines; $lines = \@lines; # run through the sugar stack one by one # first, user supplied sugar: if ( exists $rc{SYNTACTIC_SUGAR} ) { if ( ref( $rc{SYNTACTIC_SUGAR} ) ne 'ARRAY' ) { _warn "bad syntax for specifying sugar scripts; see docs"; } else { for my $s ( @{ $rc{SYNTACTIC_SUGAR} } ) { # perl-ism; apart from keeping the full path separate from the # simple name, this also protects %rc from change by implicit # aliasing, which would happen if you touched $s itself my $sfp = _which( "syntactic-sugar/$s", 'r' ); _warn("skipped sugar script '$s'"), next if not -r $sfp; $lines = SugarBox::run_sugar_script( $sfp, $lines ); $lines = [ grep /\S/, map { cleanup_conf_line($_) } @$lines ]; } } } # then our stuff: $lines = rw_cdm($lines); $lines = option($lines); # must come after rw_cdm $lines = owner_desc($lines); $lines = name_vref($lines); $lines = role_names($lines); return $lines; } sub rw_cdm { my $lines = shift; my @ret; # repo foo <...> RWC = ... # -> option CREATE_IS_C = 1 # (and similarly DELETE_IS_D and MERGE_CHECK) # but only once per repo of course my %seen = (); for my $line (@$lines) { push @ret, $line; if ( $line =~ /^repo / ) { %seen = (); } elsif ( $line =~ /^(-|C|R|RW\+?(?:C?D?|D?C?)M?) (.* )?= (.+)/ ) { my $perms = $1; push @ret, "option DELETE_IS_D = 1" if $perms =~ /D/ and not $seen{D}++; push @ret, "option CREATE_IS_C = 1" if $perms =~ /RW.*C/ and not $seen{C}++; push @ret, "option MERGE_CHECK = 1" if $perms =~ /M/ and not $seen{M}++; } } return \@ret; } sub option { my $lines = shift; my @ret; # option foo = bar # -> config gitolite-options.foo = bar for my $line (@$lines) { if ( $line =~ /^option (\S+) = (\S.*)/ ) { push @ret, "config gitolite-options.$1 = $2"; } else { push @ret, $line; } } return \@ret; } sub owner_desc { my $lines = shift; my @ret; # owner = "owner name" # -> config gitweb.owner = owner name # desc = "some long description" # -> config gitweb.description = some long description # category = "whatever..." # -> config gitweb.category = whatever... for my $line (@$lines) { if ( $line =~ /^desc = (\S.*)/ ) { push @ret, "config gitweb.description = $1"; } elsif ( $line =~ /^owner = (\S.*)/ ) { push @ret, "config gitweb.owner = $1"; } elsif ( $line =~ /^category = (\S.*)/ ) { push @ret, "config gitweb.category = $1"; } else { push @ret, $line; } } return \@ret; } sub name_vref { my $lines = shift; my @ret; # NAME/foo = # -> VREF/NAME/foo = for my $line (@$lines) { if ( $line =~ /^(-|R\S+) \S.* = \S.*/ ) { $line =~ s( NAME/)( VREF/NAME/)g; } push @ret, $line; } return \@ret; } sub role_names { my $lines = shift; my @ret; # [] = # -> same but with "@" prepended to rolenames for my $line (@$lines) { if ( $line =~ /^(-|C|R|RW\+?(?:C?D?|D?C?)M?) (.* )?= (.+)/ ) { my ( $p, $r ) = ( $1, $2 ); my $u = ''; for ( split ' ', $3 ) { $_ = "\@$_" if $_ eq 'CREATOR' or $rc{ROLES}{$_}; $u .= " $_"; } $r ||= ''; # mind the spaces (or play safe and run cleanup_conf_line again) push @ret, cleanup_conf_line("$p $r = $u"); } else { push @ret, $line; } } return \@ret; } 1; gitolite3-3.6.1/src/lib/Gitolite/Easy.pm000066400000000000000000000151561241446647300200230ustar00rootroot00000000000000package Gitolite::Easy; # easy access to gitolite from external perl programs # ---------------------------------------------------------------------- # most/all functions in this module test $ENV{GL_USER}'s rights and # permissions so it needs to be set. # "use"-ing this module # ---------------------------------------------------------------------- # Using this module from within a gitolite trigger or command is easy; you # just need 'use lib $ENV{GL_LIBDIR};' before the 'use Gitolite::Easy;'. # # Using it from something completely outside gitolite requires a bit more # work. First, run 'gitolite query-rc -a' to find the correct values for # GL_BINDIR and GL_LIBDIR in your installation. Then use this code in your # external program, using the paths you just found: # # BEGIN { # $ENV{HOME} = "/home/git"; # or whatever is the hosting user's $HOME # $ENV{GL_BINDIR} = "/full/path/to/gitolite/src"; # $ENV{GL_LIBDIR} = "/full/path/to/gitolite/src/lib"; # } # use lib $ENV{GL_LIBDIR}; # use Gitolite::Easy; # API documentation # ---------------------------------------------------------------------- # documentation for each function is at the top of the function. # Documentation is NOT in pod format; just read the source with a nice syntax # coloring text editor and you'll be happy enough. (I do not like POD; please # don't send me patches for this aspect of the module). #<<< @EXPORT = qw( is_admin is_super_admin in_group in_role owns can_read can_write config textfile %rc say say2 _die _warn _print usage option ); #>>> use Exporter 'import'; use Gitolite::Rc; use Gitolite::Common; use Gitolite::Conf::Load; use strict; use warnings; my $user; # ---------------------------------------------------------------------- # is_admin() # return true if $ENV{GL_USER} is set and has W perms to the admin repo # shell equivalent # if gitolite access -q gitolite-admin $GL_USER W; then ... sub is_admin { valid_user(); return not( access( 'gitolite-admin', $user, 'W', 'any' ) =~ /DENIED/ ); } # is_super_admin() # (useful only if you are using delegation) # return true if $ENV{GL_USER} is set and has W perms to any file in the admin # repo # shell equivalent # if gitolite access -q gitolite-admin $GL_USER W VREF/NAME/; then ... sub is_super_admin { valid_user(); return not( access( 'gitolite-admin', $user, 'W', 'VREF/NAME/' ) =~ /DENIED/ ); } # in_group() # return true if $ENV{GL_USER} is set and is in the given group # shell equivalent # if gitolite list-memberships $GL_USER | grep -x $GROUPNAME >/dev/null; then ... sub in_group { valid_user(); my $g = shift; $g =~ s/^\@?/@/; return grep { $_ eq $g } @{ Gitolite::Conf::Load::list_memberships( '-u', $user ) }; } # in_role() # return true if $ENV{GL_USER} is set and has the given role for the given repo # shell equivalent # if gitolite list-memberships -u $GL_USER -r $GL_REPO | grep -x $ROLENAME >/dev/null; then ... sub in_role { valid_user(); my $r = shift; $r =~ s/^\@?/@/; my $repo = shift; return grep { $_ eq $r } @{ Gitolite::Conf::Load::list_memberships( "-u", $user, "-r", $repo ) }; } # owns() # return true if $ENV{GL_USER} is set and is an OWNER of the given repo. # shell equivalent (assuming GL_USER is set) # if gitolite owns $REPONAME; then ... sub owns { valid_user(); my $r = shift; # prevent unnecessary disclosure of repo existence info return 0 if repo_missing($r); return ( creator($r) eq $user or $rc{OWNER_ROLENAME} and in_role( $rc{OWNER_ROLENAME}, $r ) ); } # can_read() # return true if $ENV{GL_USER} is set and can read the given repo # shell equivalent # if gitolite access -q $REPONAME $GL_USER R; then ... sub can_read { valid_user(); my $r = shift; return not( access( $r, $user, 'R', 'any' ) =~ /DENIED/ ); } # can_write() # return true if $ENV{GL_USER} is set and can write to the given repo. # Optional second argument can be '+' to check that instead of 'W'. Optional # third argument can be a full ref name instead of 'any'. # shell equivalent # if gitolite access -q $REPONAME $GL_USER W; then ... sub can_write { valid_user(); my ( $r, $aa, $ref ) = @_; $aa ||= 'W'; $ref ||= 'any'; return not( access( $r, $user, $aa, $ref ) =~ /DENIED/ ); } # config() # given a repo and a key, return a hash containing all the git config # variables for that repo where the section+key match the regex. If none are # found, return an empty hash. If you don't want it as a regex, use \Q # appropriately # shell equivalent # foo=$(gitolite git-config -r $REPONAME foo\\.bar) sub config { my $repo = shift; my $key = shift; return () if repo_missing($repo); my $ret = git_config( $repo, $key ); return %$ret; } # ---------------------------------------------------------------------- # maintain a textfile; see comments in code for details, and calls in various # other programs (like 'motd', 'desc', and 'readme') for how to call sub textfile { my %h = @_; my $repodir; # target file _die "need file" unless $h{file}; _die "'$h{file}' contains a '/'" if $h{file} =~ m(/); _sanity($h{file}); # target file's location. This can come from one of two places: dir # (which comes from our code, so does not need to be sanitised), or repo, # which may come from the user _die "need exactly one of repo or dir" unless $h{repo} xor $h{dir}; _die "'$h{dir}' does not exist" if $h{dir} and not -d $h{dir}; if ($h{repo}) { _sanity($h{repo}); $h{dir} = "$rc{GL_REPO_BASE}/$h{repo}.git"; _die "repo '$h{repo}' does not exist" if not -d $h{dir}; my $umask = option( $h{repo}, 'umask' ); # note: using option() moves us to ADMIN_BASE, but we don't care here umask oct($umask) if $umask; } # final full file name my $f = "$h{dir}/$h{file}"; # operation _die "can't have both prompt and text" if defined $h{prompt} and defined $h{text}; if (defined $h{prompt}) { print STDERR $h{prompt}; my $t = join( "", <> ); _print($f, $t); } elsif (defined $h{text}) { _print($f, $h{text}); } else { return slurp($f) if -f $f; } return ''; } # ---------------------------------------------------------------------- sub _sanity { my $name = shift; _die "'$name' contains bad characters" if $name !~ $REPONAME_PATT; _die "'$name' ends with a '/'" if $name =~ m(/$); _die "'$name' contains '..'" if $name =~ m(\.\.); } sub valid_user { _die "GL_USER not set" unless exists $ENV{GL_USER}; $user = $ENV{GL_USER}; } 1; gitolite3-3.6.1/src/lib/Gitolite/Hooks/000077500000000000000000000000001241446647300176375ustar00rootroot00000000000000gitolite3-3.6.1/src/lib/Gitolite/Hooks/PostUpdate.pm000066400000000000000000000034121241446647300222650ustar00rootroot00000000000000package Gitolite::Hooks::PostUpdate; # everything to do with the post-update hook # ---------------------------------------------------------------------- @EXPORT = qw( post_update post_update_hook ); use Exporter 'import'; use Gitolite::Rc; use Gitolite::Common; use strict; use warnings; # ---------------------------------------------------------------------- sub post_update { trace( 3, 'post-up', @ARGV ); # this is the *real* post_update hook for gitolite tsh_try("git ls-tree --name-only master"); _die "no files/dirs called 'hooks' or 'logs' are allowed" if tsh_text() =~ /^(hooks|logs)$/m; my $hooks_changed = 0; { local $ENV{GIT_WORK_TREE} = $rc{GL_ADMIN_BASE}; tsh_try("git diff --name-only master"); $hooks_changed++ if tsh_text() =~ m(/hooks/common/); # the leading slash ensure that this hooks/common directory is below # some top level directory, not *at* the top. That's LOCAL_CODE, and # it's actual name could be anything but it doesn't matter to us. tsh_try("git checkout -f --quiet master"); } _system("gitolite compile"); _system("gitolite setup --hooks-only") if $hooks_changed; _system("gitolite trigger POST_COMPILE"); exit 0; } { my $text = ''; sub post_update_hook { if ( not $text ) { local $/ = undef; $text = ; } return $text; } } 1; __DATA__ #!/usr/bin/perl use strict; use warnings; use lib $ENV{GL_LIBDIR}; use Gitolite::Hooks::PostUpdate; # gitolite post-update hook (only for the admin repo) # ---------------------------------------------------------------------- post_update(); # is not expected to return exit 1; # so if it does, something is wrong gitolite3-3.6.1/src/lib/Gitolite/Hooks/Update.pm000066400000000000000000000121561241446647300214240ustar00rootroot00000000000000package Gitolite::Hooks::Update; # everything to do with the update hook # ---------------------------------------------------------------------- @EXPORT = qw( update update_hook ); use Exporter 'import'; use Gitolite::Rc; use Gitolite::Common; use Gitolite::Conf::Load; use strict; use warnings; # ---------------------------------------------------------------------- sub update { # this is the *real* update hook for gitolite bypass() if $ENV{GL_BYPASS_ACCESS_CHECKS}; my ( $ref, $oldsha, $newsha, $oldtree, $newtree, $aa ) = args(@ARGV); trace( 2, $ENV{GL_REPO}, $ENV{GL_USER}, $aa, @ARGV ); my $ret = access( $ENV{GL_REPO}, $ENV{GL_USER}, $aa, $ref ); trigger( 'ACCESS_2', $ENV{GL_REPO}, $ENV{GL_USER}, $aa, $ref, $ret, $oldsha, $newsha ); _die $ret if $ret =~ /DENIED/; check_vrefs( $ref, $oldsha, $newsha, $oldtree, $newtree, $aa ); gl_log( 'update', $ENV{GL_REPO}, $ENV{GL_USER}, $aa, @ARGV, $ret ); exit 0; } sub bypass { require Cwd; Cwd->import; gl_log( 'update', getcwd(), '(' . ( $ENV{USER} || '?' ) . ')', 'bypass', @ARGV ); exit 0; } sub check_vrefs { my ( $ref, $oldsha, $newsha, $oldtree, $newtree, $aa ) = @_; my $name_seen = 0; my $n_vrefs = 0; for my $vref ( vrefs( $ENV{GL_REPO}, $ENV{GL_USER} ) ) { $n_vrefs++; if ( $vref =~ m(^VREF/NAME/) ) { # this one is special; we process it right here, and only once next if $name_seen++; for my $ref ( map { chomp; s(^)(VREF/NAME/); $_; } `git diff --name-only $oldtree $newtree` ) { check_vref( $aa, $ref ); } } else { my ( $dummy, $pgm, @args ) = split '/', $vref; $pgm = _which( "VREF/$pgm", 'x' ); $pgm or _die "'$vref': helper program missing or unexecutable"; open( my $fh, "-|", $pgm, @_, $vref, @args ) or _die "'$vref': can't spawn helper program: $!"; while (<$fh>) { # print non-vref lines and skip processing (for example, # normal STDOUT by a normal update hook) unless (m(^VREF/)) { print; next; } my ( $ref, $deny_message ) = split( ' ', $_, 2 ); check_vref( $aa, $ref, $deny_message ); } close($fh) or _die $! ? "Error closing sort pipe: $!" : "$vref: helper program exit status $?"; } } return $n_vrefs; } sub check_vref { my ( $aa, $ref, $deny_message ) = @_; my $ret = access( $ENV{GL_REPO}, $ENV{GL_USER}, $aa, $ref ); trace( 2, "access($ENV{GL_REPO}, $ENV{GL_USER}, $aa, $ref)", "-> $ret" ); if ( $ret =~ /by fallthru/ ) { trace( 3, "remember, fallthru is success here!" ); return; } trigger( 'ACCESS_2', $ENV{GL_REPO}, $ENV{GL_USER}, $aa, $ref, $ret ); _die "$ret" . ( $deny_message ? "\n$deny_message" : '' ) if $ret =~ /DENIED/; } { my $text = ''; sub update_hook { if ( not $text ) { local $/ = undef; $text = ; } return $text; } } # ---------------------------------------------------------------------- sub args { my ( $ref, $oldsha, $newsha ) = @_; my ( $oldtree, $newtree, $aa ); # this is special to git -- the hash of an empty tree my $empty = '4b825dc642cb6eb9a060e54bf8d69288fbee4904'; $oldtree = $oldsha eq '0' x 40 ? $empty : $oldsha; $newtree = $newsha eq '0' x 40 ? $empty : $newsha; my $merge_base = '0' x 40; # for branch create or delete, merge_base stays at '0'x40 chomp( $merge_base = `git merge-base $oldsha $newsha` ) unless $oldsha eq '0' x 40 or $newsha eq '0' x 40; $aa = 'W'; # tag rewrite $aa = '+' if $ref =~ m(refs/tags/) and $oldsha ne ( '0' x 40 ); # non-ff push to ref (including ref delete) $aa = '+' if $oldsha ne $merge_base; $aa = 'D' if ( option( $ENV{GL_REPO}, 'DELETE_IS_D' ) ) and $newsha eq '0' x 40; $aa = 'C' if ( option( $ENV{GL_REPO}, 'CREATE_IS_C' ) ) and $oldsha eq '0' x 40; # and now "M" commits. All the other accesses (W, +, C, D) were mutually # exclusive in some sense. Sure a W could be a C or a + could be a D but # that's by design. A merge commit, however, could still be any of the # others (except a "D"). # so we have to *append* 'M' to $aa (if the repo has MERGE_CHECK in # effect and this push contains a merge inside) if ( option( $ENV{GL_REPO}, 'MERGE_CHECK' ) ) { if ( $oldsha eq '0' x 40 or $newsha eq '0' x 40 ) { _warn "ref create/delete ignored for purposes of merge-check\n"; } else { $aa .= 'M' if `git rev-list -n 1 --merges $oldsha..$newsha` =~ /./; } } return ( $ref, $oldsha, $newsha, $oldtree, $newtree, $aa ); } 1; __DATA__ #!/usr/bin/perl use strict; use warnings; use lib $ENV{GL_LIBDIR}; use Gitolite::Hooks::Update; # gitolite update hook # ---------------------------------------------------------------------- update(); # is not expected to return exit 1; # so if it does, something is wrong gitolite3-3.6.1/src/lib/Gitolite/Rc.pm000066400000000000000000000522701241446647300174640ustar00rootroot00000000000000package Gitolite::Rc; # everything to do with 'rc'. Also defines some 'constants' # ---------------------------------------------------------------------- @EXPORT = qw( %rc glrc query_rc version greeting trigger _which $REMOTE_COMMAND_PATT $REF_OR_FILENAME_PATT $REPONAME_PATT $REPOPATT_PATT $USERNAME_PATT $UNSAFE_PATT ); use Exporter 'import'; use Gitolite::Common; # ---------------------------------------------------------------------- our %rc; our $non_core; # ---------------------------------------------------------------------- # pre-populate some important rc keys # ---------------------------------------------------------------------- $rc{GL_BINDIR} = $ENV{GL_BINDIR}; $rc{GL_LIBDIR} = $ENV{GL_LIBDIR}; # these keys could be overridden by the rc file later $rc{GL_REPO_BASE} = "$ENV{HOME}/repositories"; $rc{GL_ADMIN_BASE} = "$ENV{HOME}/.gitolite"; $rc{LOG_TEMPLATE} = "$ENV{HOME}/.gitolite/logs/gitolite-%y-%m.log"; # variables that should probably never be changed but someone will want to, I'll bet... # ---------------------------------------------------------------------- #<<< $REMOTE_COMMAND_PATT = qr(^[-0-9a-zA-Z._\@/+ :,\%=]*$); $REF_OR_FILENAME_PATT = qr(^[0-9a-zA-Z][-0-9a-zA-Z._\@/+ :,]*$); $REPONAME_PATT = qr(^\@?[0-9a-zA-Z][-0-9a-zA-Z._\@/+]*$); $REPOPATT_PATT = qr(^\@?[[0-9a-zA-Z][-0-9a-zA-Z._\@/+\\^$|()[\]*?{},]*$); $USERNAME_PATT = qr(^\@?[0-9a-zA-Z][-0-9a-zA-Z._\@+]*$); $UNSAFE_PATT = qr([`~#\$\&()|;<>]); #>>> # ---------------------------------------------------------------------- # find the rc file and 'do' it # ---------------------------------------------------------------------- my $current_data_version = "3.2"; my $rc = glrc('filename'); if ( -r $rc and -s $rc ) { do $rc or die $@; } if ( defined($GL_ADMINDIR) ) { say2 ""; say2 "FATAL: '$rc' seems to be for older gitolite; please see doc/g2migr.mkd\n" . "(online at http://gitolite.com/gitolite/g2migr.html)"; exit 1; } # let values specified in rc file override our internal ones # ---------------------------------------------------------------------- @rc{ keys %RC } = values %RC; # expand the non_core list into INPUT, PRE_GIT, etc using 'ENABLE' settings non_core_expand() if $rc{ENABLE}; # add internal triggers # ---------------------------------------------------------------------- # is the server/repo in a writable state (i.e., not down for maintenance etc) unshift @{ $rc{ACCESS_1} }, 'Writable::access_1'; # (testing only) override the rc file silently # ---------------------------------------------------------------------- # use an env var that is highly unlikely to appear in real life :) do $ENV{G3T_RC} if exists $ENV{G3T_RC} and -r $ENV{G3T_RC}; # setup some perl/rc/env vars, plus umask # ---------------------------------------------------------------------- umask ( $rc{UMASK} || 0077 ); unshift @INC, "$rc{LOCAL_CODE}/lib" if $rc{LOCAL_CODE}; $ENV{PATH} = "$ENV{GL_BINDIR}:$ENV{PATH}" unless $ENV{PATH} =~ /^$ENV{GL_BINDIR}:/; { $rc{GL_TID} = $ENV{GL_TID} ||= $$; # TID: loosely, transaction ID. The first PID at the entry point passes # it down to all its children so you can track each access, across all the # various commands it spawns and actions it generates. $rc{GL_LOGFILE} = $ENV{GL_LOGFILE} ||= gen_lfn( $rc{LOG_TEMPLATE} ); } # these two are meant to help externally written commands (see # src/commands/writable for an example) $ENV{GL_REPO_BASE} = $rc{GL_REPO_BASE}; $ENV{GL_ADMIN_BASE} = $rc{GL_ADMIN_BASE}; # ---------------------------------------------------------------------- use strict; use warnings; # ---------------------------------------------------------------------- my $glrc_default_text = ''; { local $/ = undef; $glrc_default_text = ; } # ---------------------------------------------------------------------- sub non_core_expand { my %enable; for my $e ( @{ $rc{ENABLE} } ) { my ( $name, $arg ) = split ' ', $e, 2; # store args as the hash value for the name $enable{$name} = $arg || ''; # for now, we pretend everything is a command, because commands # are the only thing that the non_core list does not contain $rc{COMMANDS}{$name} = $arg || 1; } # bring in additional non-core specs from the rc file, if given if ( my $nc2 = $rc{NON_CORE} ) { for ( $non_core, $nc2 ) { # beat 'em into shape :) s/#.*//g; s/[ \t]+/ /g; s/^ //mg; s/ $//mg; s/\n+/\n/g; } for ( split "\n", $nc2 ) { next unless /\S/; my ( $name, $where, $module, $before, $name2 ) = split ' ', $_; if ( not $before ) { $non_core .= "$name $where $module\n"; next; } die if $before ne 'before'; $non_core =~ s(^(?=$name2 $where( |$)))($name $where $module\n)m; } } my @data = split "\n", $non_core || ''; for (@data) { next if /^\s*(#|$)/; my ( $name, $where, $module ) = split ' ', $_; # if it appears here, it's not a command, so delete it. At the end of # this loop, what's left in $rc{COMMANDS} will be those names in the # enable list that do not appear in the non_core list. delete $rc{COMMANDS}{$name}; next unless exists $enable{$name}; # module to call is name if specified as "." $module = $name if $module eq "."; # module to call is "name::pre_git" or such if specified as "::" ( $module = $name ) .= "::" . lc($where) if $module eq '::'; # append arguments, if supplied $module .= " $enable{$name}" if $enable{$name}; push @{ $rc{$where} }, $module; } # finally, add in commands that were declared in the non-core list map { /^(\S+)/; $rc{COMMANDS}{$1} = 1 } @{ $rc{COMMAND} }; } # exported functions # ---------------------------------------------------------------------- sub glrc { my $cmd = shift; if ( $cmd eq 'default-filename' ) { return "$ENV{HOME}/.gitolite.rc"; } elsif ( $cmd eq 'default-text' ) { return $glrc_default_text if $glrc_default_text; _die "rc file default text not set; this should not happen!"; } elsif ( $cmd eq 'filename' ) { # where is the rc file? # search $HOME first return "$ENV{HOME}/.gitolite.rc" if -f "$ENV{HOME}/.gitolite.rc"; return ''; } elsif ( $cmd eq 'current-data-version' ) { return $current_data_version; } else { _die "unknown argument to glrc: '$cmd'"; } } my $all = 0; my $nonl = 0; my $quiet = 0; sub query_rc { my @vars = args(); no strict 'refs'; if ($all) { for my $e ( sort keys %rc ) { print "$e=" . ( defined( $rc{$e} ) ? $rc{$e} : 'undef' ) . "\n"; } exit 0; } my $cv = \%rc; # current "value" while (@vars) { my $v = shift @vars; # dig into the rc hash, using each var as a component if ( not ref($cv) ) { _warn "unused arguments..."; last; } elsif ( ref($cv) eq 'HASH' ) { $cv = $cv->{$v} || ''; } elsif ( ref($cv) eq 'ARRAY' ) { $cv = $cv->[$v] || ''; } else { _die "dont know what to do with " . ref($cv) . " item in the rc file"; } } # we've run out of arguments so $cv is what we have. If we're supposed to # be quiet, we don't have to print anything so let's get that done first: exit( $cv ? 0 : 1 ) if $quiet; # shell truth # print values (notice we ignore the '-n' option if it's a ref) if ( ref($cv) eq 'HASH' ) { print join( "\n", sort keys %$cv ), "\n" if %$cv; } elsif ( ref($cv) eq 'ARRAY' ) { print join( "\n", @$cv ), "\n" if @$cv; } else { print $cv . ( $nonl ? '' : "\n" ) if $cv; } exit( $cv ? 0 : 1 ); # shell truth } sub version { my $version = ''; $version = '(unknown)'; for ("$ENV{GL_BINDIR}/VERSION") { $version = slurp($_) if -r $_; } chomp($version); return $version; } sub greeting { my $json = shift; chomp( my $hn = `hostname -s 2>/dev/null || hostname` ); my $gv = substr( `git --version`, 12 ); my $gl_user = $ENV{GL_USER} || ''; $gl_user = " $gl_user" if $gl_user; if ($json) { $json->{GL_USER} = $ENV{GL_USER}; $json->{USER} = ( $ENV{USER} || "httpd" ) . "\@$hn"; $json->{gitolite_version} = version(); chomp( $json->{git_version} = $gv ); # this thing has a newline at the end return; } # normal output return "hello$gl_user, this is " . ( $ENV{USER} || "httpd" ) . "\@$hn running gitolite3 " . version() . " on git $gv\n"; } sub trigger { my $rc_section = shift; # if arg-2 (now arg-1, due to the 'shift' above) exists, it is a repo # name, so setup env from options require Gitolite::Conf::Load; Gitolite::Conf::Load->import('env_options'); env_options( $_[0] ) if $_[0]; if ( exists $rc{$rc_section} ) { if ( ref( $rc{$rc_section} ) ne 'ARRAY' ) { _die "'$rc_section' section in rc file is not a perl list"; } else { for my $s ( @{ $rc{$rc_section} } ) { my ( $pgm, @args ) = split ' ', $s; if ( my ( $module, $sub ) = ( $pgm =~ /^(.*)::(\w+)$/ ) ) { require Gitolite::Triggers; trace( 2, 'trigger module', $module, $sub, @args, $rc_section, @_ ); Gitolite::Triggers::run( $module, $sub, @args, $rc_section, @_ ); } else { $pgm = _which( "triggers/$pgm", 'x' ); _warn("skipped trigger '$s' (not found or not executable)"), next if not $pgm; trace( 2, 'trigger command', $s ); _system( $pgm, @args, $rc_section, @_ ); # they better all return with 0 exit codes! } } } return; } trace( 3, "'$rc_section' not found in rc" ); } sub _which { # looks for a file in LOCAL_CODE or GL_BINDIR. Returns whichever exists # (LOCAL_CODE preferred if defined) or 0 if not found. my $file = shift; my $mode = shift; # could be 'x' or 'r' my @files = ("$rc{GL_BINDIR}/$file"); unshift @files, ("$rc{LOCAL_CODE}/$file") if $rc{LOCAL_CODE}; for my $f (@files) { return $f if -x $f; return $f if -r $f and $mode eq 'r'; } return 0; } # ---------------------------------------------------------------------- =for args Usage: gitolite query-rc -a gitolite query-rc [-n] [-q] rc-variable -a print all variables and values (first level only) -n do not append a newline if variable is scalar -q exit code only (shell truth; 0 is success) Query the rc hash. Second and subsequent arguments dig deeper into the hash. The examples are for the default configuration; yours may be different. Single values: gitolite query-rc GL_ADMIN_BASE # prints "/home/git/.gitolite" or similar gitolite query-rc UMASK # prints "63" (that's 0077 in decimal!) Hashes: gitolite query-rc COMMANDS # prints "desc", "help", "info", "perms", "writable", one per line gitolite query-rc COMMANDS help # prints 1 gitolite query-rc -q COMMANDS help # prints nothing; exit code is 0 gitolite query-rc COMMANDS fork # prints nothing; exit code is 1 Arrays (somewhat less useful): gitolite query-rc POST_GIT # prints nothing; exit code is 0 gitolite query-rc POST_COMPILE # prints 4 lines gitolite query-rc POST_COMPILE 0 # prints the first of those 4 lines Explore: gitolite query-rc -a # prints all first level variables and values, one per line. Any that are # listed as HASH or ARRAY can be explored further in subsequent commands. =cut sub args { my $help = 0; require Getopt::Long; Getopt::Long::GetOptions( 'all|a' => \$all, 'nonl|n' => \$nonl, 'quiet|q' => \$quiet, 'help|h' => \$help, ) or usage(); usage("'-a' cannot be combined with other arguments or options") if $all and ( @ARGV or $nonl or $quiet ); usage() if not $all and not @ARGV or $help; return @ARGV; } # ---------------------------------------------------------------------- BEGIN { $non_core = " # No user-servicable parts inside. Warranty void if seal broken. Refer # servicing to authorised service center only. continuation-lines SYNTACTIC_SUGAR . keysubdirs-as-groups SYNTACTIC_SUGAR . macros SYNTACTIC_SUGAR . refex-expr SYNTACTIC_SUGAR . renice PRE_GIT . Kindergarten INPUT :: CpuTime INPUT :: CpuTime POST_GIT :: Shell INPUT :: Alias INPUT :: Motd INPUT :: Motd PRE_GIT :: Motd COMMAND motd Mirroring INPUT :: Mirroring PRE_GIT :: Mirroring POST_GIT :: refex-expr ACCESS_2 RefexExpr::access_2 expand-deny-messages ACCESS_1 . expand-deny-messages ACCESS_2 . RepoUmask PRE_GIT :: RepoUmask POST_CREATE :: partial-copy PRE_GIT . upstream PRE_GIT . no-create-on-read PRE_CREATE AutoCreate::deny_R no-auto-create PRE_CREATE AutoCreate::deny_RW ssh-authkeys-split POST_COMPILE post-compile/ssh-authkeys-split ssh-authkeys POST_COMPILE post-compile/ssh-authkeys Shell POST_COMPILE post-compile/ssh-authkeys-shell-users set-default-roles POST_CREATE . git-config POST_COMPILE post-compile/update-git-configs git-config POST_CREATE post-compile/update-git-configs gitweb POST_CREATE post-compile/update-gitweb-access-list gitweb POST_COMPILE post-compile/update-gitweb-access-list cgit POST_COMPILE post-compile/update-description-file daemon POST_CREATE post-compile/update-git-daemon-access-list daemon POST_COMPILE post-compile/update-git-daemon-access-list repo-specific-hooks POST_COMPILE . repo-specific-hooks POST_CREATE . "; } 1; # ---------------------------------------------------------------------- __DATA__ # configuration variables for gitolite # This file is in perl syntax. But you do NOT need to know perl to edit it -- # just mind the commas, use single quotes unless you know what you're doing, # and make sure the brackets and braces stay matched up! # (Tip: perl allows a comma after the last item in a list also!) # HELP for commands can be had by running the command with "-h". # HELP for all the other FEATURES can be found in the documentation (look for # "list of non-core programs shipped with gitolite" in the master index) or # directly in the corresponding source file. %RC = ( # ------------------------------------------------------------------ # default umask gives you perms of '0700'; see the rc file docs for # how/why you might change this UMASK => 0077, # look for "git-config" in the documentation GIT_CONFIG_KEYS => '', # comment out if you don't need all the extra detail in the logfile LOG_EXTRA => 1, # syslog options # 1. leave this section as is for normal gitolite logging # 2. uncomment this line to log only to syslog: # LOG_DEST => 'syslog', # 3. uncomment this line to log to syslog and the normal gitolite log: # LOG_DEST => 'syslog,normal', # roles. add more roles (like MANAGER, TESTER, ...) here. # WARNING: if you make changes to this hash, you MUST run 'gitolite # compile' afterward, and possibly also 'gitolite trigger POST_COMPILE' ROLES => { READERS => 1, WRITERS => 1, }, # enable caching (currently only Redis). PLEASE RTFM BEFORE USING!!! # CACHE => 'Redis', # ------------------------------------------------------------------ # rc variables used by various features # the 'info' command prints this as additional info, if it is set # SITE_INFO => 'Please see http://blahblah/gitolite for more help', # the CpuTime feature uses these # display user, system, and elapsed times to user after each git operation # DISPLAY_CPU_TIME => 1, # display a warning if total CPU times (u, s, cu, cs) crosses this limit # CPU_TIME_WARN_LIMIT => 0.1, # the Mirroring feature needs this # HOSTNAME => "foo", # TTL for redis cache; PLEASE SEE DOCUMENTATION BEFORE UNCOMMENTING! # CACHE_TTL => 600, # ------------------------------------------------------------------ # suggested locations for site-local gitolite code (see cust.html) # this one is managed directly on the server # LOCAL_CODE => "$ENV{HOME}/local", # or you can use this, which lets you put everything in a subdirectory # called "local" in your gitolite-admin repo. For a SECURITY WARNING # on this, see http://gitolite.com/gitolite/cust.html#pushcode # LOCAL_CODE => "$rc{GL_ADMIN_BASE}/local", # ------------------------------------------------------------------ # List of commands and features to enable ENABLE => [ # COMMANDS # These are the commands enabled by default 'help', 'desc', 'info', 'perms', 'writable', # Uncomment or add new commands here. # 'create', # 'fork', # 'mirror', # 'readme', # 'sskm', # 'D', # These FEATURES are enabled by default. # essential (unless you're using smart-http mode) 'ssh-authkeys', # creates git-config enties from gitolite.conf file entries like 'config foo.bar = baz' 'git-config', # creates git-daemon-export-ok files; if you don't use git-daemon, comment this out 'daemon', # creates projects.list file; if you don't use gitweb, comment this out 'gitweb', # These FEATURES are disabled by default; uncomment to enable. If you # need to add new ones, ask on the mailing list :-) # user-visible behaviour # prevent wild repos auto-create on fetch/clone # 'no-create-on-read', # no auto-create at all (don't forget to enable the 'create' command!) # 'no-auto-create', # access a repo by another (possibly legacy) name # 'Alias', # give some users direct shell access. See documentation in # sts.html for details on the following two choices. # "Shell $ENV{HOME}/.gitolite.shell-users", # 'Shell alice bob', # set default roles from lines like 'option default.roles-1 = ...', etc. # 'set-default-roles', # show more detailed messages on deny # 'expand-deny-messages', # show a message of the day # 'Motd', # system admin stuff # enable mirroring (don't forget to set the HOSTNAME too!) # 'Mirroring', # allow people to submit pub files with more than one key in them # 'ssh-authkeys-split', # selective read control hack # 'partial-copy', # manage local, gitolite-controlled, copies of read-only upstream repos # 'upstream', # updates 'description' file instead of 'gitweb.description' config item # 'cgit', # allow repo-specific hooks to be added # 'repo-specific-hooks', # performance, logging, monitoring... # be nice # 'renice 10', # log CPU times (user, system, cumulative user, cumulative system) # 'CpuTime', # syntactic_sugar for gitolite.conf and included files # allow backslash-escaped continuation lines in gitolite.conf # 'continuation-lines', # create implicit user groups from directory names in keydir/ # 'keysubdirs-as-groups', # allow simple line-oriented macros # 'macros', # Kindergarten mode # disallow various things that sensible people shouldn't be doing anyway # 'Kindergarten', ], ); # ------------------------------------------------------------------------------ # per perl rules, this should be the last line in such a file: 1; # Local variables: # mode: perl # End: # vim: set syn=perl: gitolite3-3.6.1/src/lib/Gitolite/Setup.pm000066400000000000000000000114351241446647300202160ustar00rootroot00000000000000package Gitolite::Setup; # implements 'gitolite setup' # ---------------------------------------------------------------------- =for args Usage: gitolite setup [