├── .github ├── dependabot.yml └── workflows │ ├── go-test.yml │ └── golangci-lint.yml ├── .gitignore ├── CODEOWNERS ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── Makefile ├── README.md ├── SECURITY.md ├── SUPPORT.md ├── cmd ├── spokes-receive-pack-networked-wrapper │ └── main.go └── spokes-receive-pack-wrapper │ └── main.go ├── go.mod ├── go.sum ├── internal ├── config │ ├── git.go │ └── git_test.go ├── governor │ ├── conn.go │ ├── conn_test.go │ ├── governor.go │ ├── governor_test.go │ ├── procstats.go │ └── procstats_linux.go ├── integration │ ├── capabilities_test.go │ ├── chdir.go │ ├── hiderefs_test.go │ ├── integration_networked_repo_test.go │ ├── integration_test.go │ ├── logshim.go │ ├── missingobjects_test.go │ ├── nosideband_test.go │ ├── parse.go │ ├── pushoptions_test.go │ ├── testdata │ │ ├── bad-date │ │ │ ├── sha1.git │ │ │ │ ├── HEAD │ │ │ │ ├── config │ │ │ │ ├── description │ │ │ │ ├── info │ │ │ │ │ └── refs │ │ │ │ ├── objects │ │ │ │ │ ├── info │ │ │ │ │ │ └── packs │ │ │ │ │ └── pack │ │ │ │ │ │ ├── pack-01f7c523f4ab85d86dbe8b116ab6cd69e87497f3.bitmap │ │ │ │ │ │ ├── pack-01f7c523f4ab85d86dbe8b116ab6cd69e87497f3.idx │ │ │ │ │ │ └── pack-01f7c523f4ab85d86dbe8b116ab6cd69e87497f3.pack │ │ │ │ ├── packed-refs │ │ │ │ └── refs │ │ │ │ │ └── .keep │ │ │ └── sha256.git │ │ │ │ ├── HEAD │ │ │ │ ├── config │ │ │ │ ├── description │ │ │ │ ├── info │ │ │ │ └── refs │ │ │ │ ├── objects │ │ │ │ ├── info │ │ │ │ │ └── packs │ │ │ │ └── pack │ │ │ │ │ ├── pack-1a1bc6a0c21b6c3b7683df993586a57ec40580603ee77348431ab8d4f72b55f0.bitmap │ │ │ │ │ ├── pack-1a1bc6a0c21b6c3b7683df993586a57ec40580603ee77348431ab8d4f72b55f0.idx │ │ │ │ │ └── pack-1a1bc6a0c21b6c3b7683df993586a57ec40580603ee77348431ab8d4f72b55f0.pack │ │ │ │ ├── packed-refs │ │ │ │ └── refs │ │ │ │ └── .keep │ │ ├── empty.pack │ │ ├── gitconfig │ │ ├── missing-objects │ │ │ ├── bad.pack │ │ │ ├── empty.pack │ │ │ ├── info.json │ │ │ └── remote.git │ │ │ │ ├── HEAD │ │ │ │ ├── config │ │ │ │ ├── description │ │ │ │ ├── info │ │ │ │ └── refs │ │ │ │ ├── objects │ │ │ │ ├── info │ │ │ │ │ └── packs │ │ │ │ └── pack │ │ │ │ │ ├── pack-3e83f112afcee29136d3e21f295a9ba4d76ce184.bitmap │ │ │ │ │ ├── pack-3e83f112afcee29136d3e21f295a9ba4d76ce184.idx │ │ │ │ │ └── pack-3e83f112afcee29136d3e21f295a9ba4d76ce184.pack │ │ │ │ ├── packed-refs │ │ │ │ └── refs │ │ │ │ └── .keep │ │ ├── remote │ │ │ ├── git-internals-fork.git │ │ │ │ ├── HEAD │ │ │ │ ├── config │ │ │ │ ├── description │ │ │ │ ├── hooks │ │ │ │ │ ├── applypatch-msg.sample │ │ │ │ │ ├── commit-msg.sample │ │ │ │ │ ├── fsmonitor-watchman.sample │ │ │ │ │ ├── post-update.sample │ │ │ │ │ ├── pre-applypatch.sample │ │ │ │ │ ├── pre-commit.sample │ │ │ │ │ ├── pre-merge-commit.sample │ │ │ │ │ ├── pre-push.sample │ │ │ │ │ ├── pre-rebase.sample │ │ │ │ │ ├── pre-receive.sample │ │ │ │ │ ├── prepare-commit-msg.sample │ │ │ │ │ ├── push-to-checkout.sample │ │ │ │ │ └── update.sample │ │ │ │ ├── info │ │ │ │ │ └── exclude │ │ │ │ ├── objects │ │ │ │ │ └── info │ │ │ │ │ │ └── alternates │ │ │ │ ├── packed-refs │ │ │ │ └── refs │ │ │ │ │ ├── heads │ │ │ │ │ └── .gitkeep │ │ │ │ │ └── tags │ │ │ │ │ └── .gitkeep │ │ │ ├── git-internals.git │ │ │ │ ├── HEAD │ │ │ │ ├── config │ │ │ │ ├── objects │ │ │ │ │ └── info │ │ │ │ │ │ └── alternates │ │ │ │ └── packed-refs │ │ │ └── network.git │ │ │ │ ├── HEAD │ │ │ │ ├── config │ │ │ │ ├── description │ │ │ │ ├── objects │ │ │ │ ├── 13 │ │ │ │ │ └── 50f603d9d4712f9219861291e874702427e455 │ │ │ │ ├── 81 │ │ │ │ │ └── 125cd7d188fc6c83fda2a97d0f3f6267713532 │ │ │ │ ├── 96 │ │ │ │ │ └── 9206b2584b8293d677b3f7bbdc6017b8c573f6 │ │ │ │ ├── 05 │ │ │ │ │ └── 9ea99a3c9151f96b2a4f99ea9604a4fff99306 │ │ │ │ ├── 3a │ │ │ │ │ └── a69c529d9a10c64443ec36e7e3599ac60680ab │ │ │ │ ├── 4f │ │ │ │ │ └── 37ac0f4282b2ac78e9242f2b4d570e23d6552c │ │ │ │ ├── 9a │ │ │ │ │ └── e7a27e1095e1e20c099c5bff79c3725825eb6b │ │ │ │ ├── 9d │ │ │ │ │ └── 6d97ef453d420f2b7e3a77b0a68ff1035f78f1 │ │ │ │ ├── a3 │ │ │ │ │ └── 668948e211f5dffc4a66109b97fd23693531ae │ │ │ │ ├── d7 │ │ │ │ │ └── 2ffbc4a701ecb9580058cfddc1d63e0b5ad817 │ │ │ │ ├── e5 │ │ │ │ │ └── 89bdee50e39beac56220c4b7a716225f79e3cf │ │ │ │ ├── e8 │ │ │ │ │ └── 6fe46ef42e3f721af78091d77cff13d95f9db3 │ │ │ │ └── info │ │ │ │ │ └── commit-graphs │ │ │ │ │ ├── commit-graph-chain │ │ │ │ │ └── graph-ebb808245825422c9c8704c510ec9118e52e24cd.graph │ │ │ │ └── refs │ │ │ │ └── remotes │ │ │ │ ├── git-internals-fork │ │ │ │ └── heads │ │ │ │ │ ├── branch-1 │ │ │ │ │ ├── branch-2 │ │ │ │ │ └── main │ │ │ │ └── git-internals │ │ │ │ └── heads │ │ │ │ ├── branch-1 │ │ │ │ ├── branch-2 │ │ │ │ └── main │ │ ├── set-up-bad-date-push │ │ └── set-up-missing-objects-push │ └── util.go ├── objectformat │ ├── git.go │ └── git_test.go ├── pktline │ ├── capabilities.go │ ├── capabilities_test.go │ ├── pktline.go │ └── pktline_test.go ├── receivepack │ └── receivepack.go ├── sockstat │ ├── sockstat.go │ └── sockstat_test.go └── spokes │ ├── spokes.go │ ├── spokes_test.go │ └── testdata │ └── lots-of-refs.git │ ├── HEAD │ ├── config │ ├── description │ ├── info │ ├── exclude │ └── refs │ ├── objects │ ├── info │ │ └── packs │ └── pack │ │ ├── pack-714209910910d57afd6c8c83dcce4057d4f10c0b.bitmap │ │ ├── pack-714209910910d57afd6c8c83dcce4057d4f10c0b.idx │ │ └── pack-714209910910d57afd6c8c83dcce4057d4f10c0b.pack │ ├── packed-refs │ └── refs │ └── heads │ └── main ├── ownership.yaml └── spokes-receive-pack.go /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 2 3 | updates: 4 | - package-ecosystem: github-actions 5 | directory: "/" 6 | schedule: 7 | interval: weekly 8 | allow: 9 | # Allow updates for GitHub-owned actions 10 | - dependency-name: "actions/*" 11 | - dependency-name: "github/*" 12 | - package-ecosystem: gomod 13 | directory: "/" 14 | schedule: 15 | interval: weekly 16 | -------------------------------------------------------------------------------- /.github/workflows/go-test.yml: -------------------------------------------------------------------------------- 1 | name: go test 2 | 3 | permissions: 4 | contents: read 5 | 6 | on: 7 | push: 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v4 15 | - uses: actions/setup-go@v5 16 | with: 17 | go-version: "1.21" 18 | - name: Install git. 19 | run: | 20 | sudo apt-get install -y libcurl4-openssl-dev 21 | git clone https://github.com/git/git.git 22 | git -C git checkout next 23 | sudo make -j 16 -C git prefix=/usr NO_GETTEXT=YesPlease all install 24 | - name: Vendor modules for later steps. 25 | run: | 26 | go mod vendor 27 | - name: go unit test 28 | run: make test 29 | - name: go integration test 30 | run: make test-integration 31 | 32 | -------------------------------------------------------------------------------- /.github/workflows/golangci-lint.yml: -------------------------------------------------------------------------------- 1 | name: golangci-lint 2 | 3 | permissions: 4 | contents: read 5 | 6 | on: 7 | push: 8 | 9 | jobs: 10 | golangci: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v4 15 | - uses: actions/setup-go@v5 16 | with: 17 | go-version: "1.24" 18 | - name: Vendor modules for later steps. 19 | run: | 20 | go mod vendor 21 | - uses: golangci/golangci-lint-action@2226d7cb06a077cd73e56eedd38eecad18e5d837 # v6.5.0 22 | with: 23 | # Required: the version of golangci-lint is required and must be specified without patch version: we always use the latest patch version. 24 | version: v1.64 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.cache 2 | /.idea 3 | /bin 4 | /build 5 | /man 6 | /tmp 7 | /test/tmp 8 | *.swp 9 | **/testdata/**/gitk.cache 10 | Brewfile.lock.json 11 | tools 12 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | # This repository is maintained by: 2 | * @github/git-access 3 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at opensource@github.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at [http://contributor-covenant.org/version/1/4][version] 72 | 73 | [homepage]: http://contributor-covenant.org 74 | [version]: http://contributor-covenant.org/version/1/4/ 75 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | At this time, we aren't accepting community contributions to the project. 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SHELL = /bin/sh 2 | 3 | NW_TEST_RUNNERS = 12 4 | 5 | GO := go 6 | M := $(shell printf "\033[34;1m▶\033[0m") 7 | 8 | BIN := $(CURDIR)/bin 9 | 10 | # APPVERSION can be used by an app's build target. It uses the git sha of HEAD. 11 | APPVERSION := $(or $(shell git rev-parse HEAD 2>/dev/null),"unknown") 12 | 13 | # Add items to this variable to add to the `clean` target: 14 | CLEAN := 15 | 16 | # Generic Go binaries 17 | GO_BINARIES := \ 18 | $(BIN)/spokes-receive-pack-wrapper \ 19 | $(BIN)/spokes-receive-pack-networked-wrapper 20 | 21 | EXECUTABLES := \ 22 | $(BIN)/spokes-receive-pack 23 | 24 | CLEAN += $(EXECUTABLES) 25 | 26 | .PHONY: FORCE 27 | 28 | .PHONY: all 29 | all: info 30 | all: $(EXECUTABLES) 31 | 32 | .PHONY: info 33 | info: 34 | $(GO) version 35 | git --version 36 | 37 | $(BIN): 38 | mkdir -p $(BIN) 39 | 40 | ########################################################################### 41 | 42 | # Build binaries 43 | # 44 | # We need to compile using `go build` rather than `go install`, 45 | # because the latter doesn't work for cross-compiling. 46 | 47 | # Build the main service app: 48 | $(BIN)/spokes-receive-pack: FORCE | $(BIN) 49 | $(GO) build -ldflags '-X main.BuildVersion=$(APPVERSION)' -o $@ . 50 | 51 | # Build other generic Go binaries: 52 | .PHONY: go-binaries 53 | go-binaries: $(GO_BINARIES) 54 | $(GO_BINARIES): $(BIN)/%: cmd/% FORCE | $(BIN) 55 | $(GO) build $(BUILDTAGS) -o $@ ./$< 56 | 57 | 58 | ########################################################################### 59 | 60 | # Testing 61 | FAILPOINT_ENABLE := tools/bin/failpoint-ctl enable 62 | FAILPOINT_DISABLE := tools/bin/failpoint-ctl disable 63 | 64 | tools/bin/failpoint-ctl: 65 | GOBIN=$(shell pwd)/tools/bin $(GO) install github.com/pingcap/failpoint/failpoint-ctl@v0.0.0-20220801062533-2eaa32854a6c 66 | 67 | failpoint-enable: tools/bin/failpoint-ctl 68 | @echo "$(M) enabling failpoints..." 69 | @$(FAILPOINT_ENABLE) 70 | 71 | failpoint-disable: tools/bin/failpoint-ctl 72 | @echo "$(M) disabling failpoints..." 73 | @$(FAILPOINT_DISABLE) 74 | 75 | .PHONY: test 76 | test: go-test 77 | test-integration: BUILDTAGS=-tags integration 78 | test-integration: failpoint-enable all go-binaries go-test-integration 79 | 80 | TESTFLAGS := -race -timeout 60s 81 | TESTINTEGRATIONFLAGS := $(TESTFLAGS) --tags=integration 82 | TESTSUITE := ./... 83 | .PHONY: go-test 84 | go-test: 85 | @echo "$(M) running tests..." 86 | $(GO) test $(TESTFLAGS) $(TESTSUITE) 2>&1 87 | 88 | go-test-integration: 89 | @echo "$(M) running integration tests..." 90 | 91 | # Add our compiled `spokes-receive-pack` to the PATH while running tests: 92 | PATH="$(CURDIR)/bin:$(PATH)" \ 93 | GIT_CONFIG_SYSTEM="$(CURDIR)/internal/integration/testdata/gitconfig" \ 94 | $(GO) test $(TESTINTEGRATIONFLAGS) $(TESTSUITE) 2>&1 95 | 96 | @echo "$(M) disabling failpoints ..." 97 | @$(FAILPOINT_DISABLE) 98 | 99 | CLEAN += log/*.log 100 | 101 | ########################################################################### 102 | 103 | # Benchmarks 104 | 105 | BENCHFLAGS := 106 | bench: 107 | @echo "$(M) running benchmarks..." 108 | $(GO) test -bench=. $(BENCHFLAGS) $(TESTSUITE) 2>&1 109 | 110 | ########################################################################### 111 | 112 | # Miscellaneous 113 | 114 | .PHONY: coverage 115 | coverage: 116 | @echo "$(M) running code coverage..." 117 | $(GO) test $(TESTFLAGS) $(TESTSUITE) -coverprofile coverage.out 2>&1 118 | $(GO) tool cover -html=coverage.out 119 | rm -f coverage.out 120 | 121 | # Profiling 122 | PPROF := $(BIN)/pprof 123 | $(PPROF): 124 | $(GO) get -u github.com/google/pprof 125 | 126 | .PHONY: pprof 127 | pprof: | $(PPROF) ## Build the pprof binary 128 | 129 | # Formatting 130 | GOFMT := $(BIN)/goimports 131 | $(BIN)/goimports: 132 | GOBIN=$(BIN) $(GO) install golang.org/x/tools/cmd/goimports 133 | 134 | # Run goimports on all source files: 135 | .PHONY: fmt 136 | fmt: | $(GOFMT) 137 | @echo "$(M) running goimports..." 138 | @ret=0 && for d in $$($(GO) list -f '{{.Dir}}' ./...); do \ 139 | $(GOFMT) -l -w $$d/*.go || ret=$$? ; \ 140 | done ; exit $$ret 141 | 142 | 143 | # Run golang-ci lint on all source files: 144 | GOLANGCILINT := $(BIN)/golangci-lint 145 | $(BIN)/golangci-lint: 146 | GOBIN=$(BIN) $(GO) install github.com/golangci/golangci-lint/cmd/golangci-lint@latest 147 | 148 | .PHONY: fmt 149 | lint: | $(GOLANGCILINT) 150 | @echo "$(M) running golangci-lint" 151 | $(GOLANGCILINT) run 152 | 153 | # Run modernize on all source files: 154 | GOLANGMODERNIZE := $(BIN)/modernize 155 | $(BIN)/modernize: 156 | GOBIN=$(BIN) $(GO) install golang.org/x/tools/gopls/internal/analysis/modernize/cmd/modernize@latest 157 | 158 | .PHONY: modernize 159 | modernize: | $(GOLANGMODERNIZE) 160 | @echo "$(M) running golangci-lint" 161 | $(GOLANGMODERNIZE) -fix -test ./... 162 | ########################################################################### 163 | 164 | # Cleanup 165 | 166 | .PHONY: clean 167 | clean: 168 | @echo "$(M) cleaning..." 169 | rm -f $(CLEAN) 170 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # spokes-receive-pack 2 | 3 | `spokes-receive-pack` is a replacement for `git-receive-pack`, used on the server side of a `git push`. `git-receive-pack` does more than we need it to, `spokes-receive-pack` implements the bits of it that we still need. 4 | 5 | We don't expect this to be generally useful outside of GitHub's service backend, and we don't plan to accept contributions from outside GitHub. 6 | 7 | ## License 8 | 9 | This project is licensed under the terms of the GPL v2 open source license. Please refer to [LICENSE](./LICENSE) for the full terms. 10 | 11 | ## Maintainers 12 | 13 | See [CODEOWNERS](./CODEOWNERS). 14 | 15 | ## Support 16 | 17 | Use of this project outside a licensed GitHub installation or the GitHub service comes with no expectation of support. 18 | 19 | ## Acknowledgement 20 | 21 | We're thankful for all the great work that's gone into the Git project and its associated tools! 22 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | Thanks for helping make GitHub safe for everyone. 2 | 3 | ## Security 4 | 5 | GitHub takes the security of our software products and services seriously, including all of the open source code repositories managed through our GitHub organizations, such as [GitHub](https://github.com/GitHub). 6 | 7 | Even though [open source repositories are outside of the scope of our bug bounty program](https://bounty.github.com/index.html#scope) and therefore not eligible for bounty rewards, we will ensure that your finding gets passed along to the appropriate maintainers for remediation. 8 | 9 | ## Reporting Security Issues 10 | 11 | If you believe you have found a security vulnerability in any GitHub-owned repository, please report it to us through coordinated disclosure. 12 | 13 | **Please do not report security vulnerabilities through public GitHub issues, discussions, or pull requests.** 14 | 15 | Instead, please send an email to opensource-security[@]github.com. 16 | 17 | Please include as much of the information listed below as you can to help us better understand and resolve the issue: 18 | 19 | * The type of issue (e.g., buffer overflow, SQL injection, or cross-site scripting) 20 | * Full paths of source file(s) related to the manifestation of the issue 21 | * The location of the affected source code (tag/branch/commit or direct URL) 22 | * Any special configuration required to reproduce the issue 23 | * Step-by-step instructions to reproduce the issue 24 | * Proof-of-concept or exploit code (if possible) 25 | * Impact of the issue, including how an attacker might exploit the issue 26 | 27 | This information will help us triage your report more quickly. 28 | 29 | ## Policy 30 | 31 | See [GitHub's Safe Harbor Policy](https://docs.github.com/en/github/site-policy/github-bug-bounty-program-legal-safe-harbor#1-safe-harbor-terms) 32 | -------------------------------------------------------------------------------- /SUPPORT.md: -------------------------------------------------------------------------------- 1 | # Getting support 2 | 3 | This project is a small component of a much larger system. 4 | As we aren't accepting community contributions and don't expect it to be very useful outside a running GitHub instance, we therefore offer no direct support. 5 | If you're having trouble with GitHub.com, a GitHub Enterprise Server, or other GitHub product, please visit https://support.github.com/ to learn about your support options. 6 | -------------------------------------------------------------------------------- /cmd/spokes-receive-pack-networked-wrapper/main.go: -------------------------------------------------------------------------------- 1 | //go:build integration 2 | 3 | package main 4 | 5 | import ( 6 | "fmt" 7 | "os" 8 | 9 | "github.com/github/spokes-receive-pack/internal/integration" 10 | ) 11 | 12 | // This little program is only a function used to pass the required environment to the actual `spokes-receive-pack` 13 | // binary during our networked integration tests 14 | func main() { 15 | env := []string{ 16 | "GIT_SOCKSTAT_VAR_spokes_quarantine=bool:true", 17 | "GIT_SOCKSTAT_VAR_quarantine_id=test_quarantine_id", 18 | "GIT_SOCKSTAT_VAR_parent_repo_id=git-internals", 19 | "GIT_NW_ADVERTISE_TAGS=true", 20 | } 21 | if err := integration.RunMain(env); err != nil { 22 | fmt.Fprintf(os.Stderr, "unexpected error running the spokes-receive-pack binary. Error: %s", err.Error()) 23 | os.Exit(1) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /cmd/spokes-receive-pack-wrapper/main.go: -------------------------------------------------------------------------------- 1 | //go:build integration 2 | 3 | package main 4 | 5 | import ( 6 | "fmt" 7 | "os" 8 | 9 | "github.com/github/spokes-receive-pack/internal/integration" 10 | ) 11 | 12 | // This little program is only a function used to pass the required environment to the actual `spokes-receive-pack` 13 | // binary during our integration tests 14 | func main() { 15 | env := []string{ 16 | "GIT_SOCKSTAT_VAR_spokes_quarantine=bool:true", 17 | "GIT_SOCKSTAT_VAR_quarantine_id=test_quarantine_id", 18 | } 19 | if err := integration.RunMain(env); err != nil { 20 | fmt.Fprintf(os.Stderr, "unexpected error running the spokes-receive-pack binary. Error: %s", err.Error()) 21 | os.Exit(1) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/github/spokes-receive-pack 2 | 3 | go 1.24 4 | 5 | require ( 6 | github.com/github/go-pipe v1.0.2 7 | github.com/pingcap/failpoint v0.0.0-20220801062533-2eaa32854a6c 8 | github.com/stretchr/testify v1.9.0 9 | golang.org/x/sync v0.13.0 10 | ) 11 | 12 | require ( 13 | github.com/davecgh/go-spew v1.1.1 // indirect 14 | github.com/pingcap/errors v0.11.4 // indirect 15 | github.com/pmezard/go-difflib v1.0.0 // indirect 16 | gopkg.in/yaml.v3 v3.0.1 // indirect 17 | ) 18 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 2 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 3 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/github/go-pipe v1.0.2 h1:befTXflsc6ir/h9f6Q7QCDmfojoBswD1MfQrPhmmSoA= 5 | github.com/github/go-pipe v1.0.2/go.mod h1:/GvNLA516QlfGGMtfv4PC/5/CdzL9X4af/AJYhmLD54= 6 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 7 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 8 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 9 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 10 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 11 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 12 | github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4= 13 | github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8= 14 | github.com/pingcap/failpoint v0.0.0-20220801062533-2eaa32854a6c h1:CgbKAHto5CQgWM9fSBIvaxsJHuGP0uM74HXtv3MyyGQ= 15 | github.com/pingcap/failpoint v0.0.0-20220801062533-2eaa32854a6c/go.mod h1:4qGtCB0QK0wBzKtFEGDhxXnSnbQApw1gc9siScUl8ew= 16 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 17 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 18 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 19 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 20 | github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= 21 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 22 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 23 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 24 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 25 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 26 | go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= 27 | go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk= 28 | go.uber.org/goleak v1.2.0/go.mod h1:XJYK+MuIchqpmGmUSAzotztawfKvYLUIgg7guXrwVUo= 29 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 30 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 31 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 32 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 33 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 34 | golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610= 35 | golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= 36 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 37 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 38 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 39 | golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 40 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 41 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 42 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 43 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= 44 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 45 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 46 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 47 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 48 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 49 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 50 | -------------------------------------------------------------------------------- /internal/config/git.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "fmt" 7 | "os/exec" 8 | "strconv" 9 | "strings" 10 | ) 11 | 12 | // ConfigEntry represents an entry in the gitconfig. 13 | type ConfigEntry struct { 14 | // Key is the entry's key, with any common `prefix` removed (see 15 | // `Config()`). 16 | Key string 17 | 18 | // Value is the entry's value, as a string. 19 | Value string 20 | } 21 | 22 | // Config represents the gitconfig, or part of the gitconfig, read by 23 | // `ReadConfig()`. 24 | type Config struct { 25 | // Entries contains the configuration entries that matched 26 | // `Prefix`, in the order that they are reported by `git config 27 | // --list`. 28 | Entries []ConfigEntry 29 | } 30 | 31 | // GetConfig returns the entries from gitconfig in the repo located at repo. 32 | func GetConfig(repo string) (*Config, error) { 33 | cmd := exec.Command( 34 | "git", 35 | "config", 36 | "--list", 37 | "-z") 38 | cmd.Dir = repo 39 | 40 | out, err := cmd.Output() 41 | if err != nil { 42 | return nil, fmt.Errorf("reading git configuration: %w", err) 43 | } 44 | 45 | config := &Config{} 46 | 47 | for len(out) > 0 { 48 | keyEnd := bytes.IndexByte(out, '\n') 49 | if keyEnd == -1 { 50 | return nil, errors.New("invalid output from 'git config'") 51 | } 52 | key := string(out[:keyEnd]) 53 | out = out[keyEnd+1:] 54 | valueEnd := bytes.IndexByte(out, 0) 55 | if valueEnd == -1 { 56 | return nil, errors.New("invalid output from 'git config'") 57 | } 58 | value := string(out[:valueEnd]) 59 | out = out[valueEnd+1:] 60 | 61 | entry := ConfigEntry{ 62 | Key: key, 63 | Value: value, 64 | } 65 | config.Entries = append(config.Entries, entry) 66 | } 67 | 68 | return config, nil 69 | } 70 | 71 | // Get returns the last entry in the list for the request config setting or an empty string in case 72 | // it cannot be found 73 | func (c *Config) Get(name string) string { 74 | name = strings.ToLower(name) 75 | value := "" 76 | for _, entry := range c.Entries { 77 | if entry.Key == name { 78 | value = entry.Value 79 | } 80 | } 81 | 82 | return value 83 | } 84 | 85 | // GetAll returns all values for the requested config setting. 86 | func (c *Config) GetAll(name string) []string { 87 | name = strings.ToLower(name) 88 | var res []string 89 | for _, entry := range c.Entries { 90 | if entry.Key == name { 91 | res = append(res, entry.Value) 92 | } 93 | } 94 | return res 95 | } 96 | func (c *Config) GetPrefix(prefix string) map[string][]string { 97 | var m = make(map[string][]string) 98 | for _, entry := range c.Entries { 99 | if strings.HasPrefix(entry.Key, prefix) { 100 | trimmedKey := strings.TrimPrefix(entry.Key, prefix) 101 | m[trimmedKey] = append(m[trimmedKey], entry.Value) 102 | } 103 | } 104 | return m 105 | } 106 | 107 | // ParseSigned parses a string that may contain a signed integer with an 108 | // optional suffix (either 'k', 'm', or 'g' for their respective IEC values). 109 | func ParseSigned(str string) (int, error) { 110 | factor := 1 111 | 112 | if len(str) > 0 { 113 | switch str[len(str)-1] { 114 | case 'k', 'K': 115 | factor = 1024 116 | case 'm', 'M': 117 | factor = 1024 * 1024 118 | case 'g', 'G': 119 | factor = 1024 * 1024 * 1024 120 | } 121 | 122 | if factor != 1 { 123 | str = str[:len(str)-1] 124 | } 125 | } 126 | 127 | n, err := strconv.Atoi(str) 128 | if err != nil { 129 | return 0, err 130 | } 131 | 132 | return n * factor, nil 133 | } 134 | -------------------------------------------------------------------------------- /internal/config/git_test.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "os/exec" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func testGetConfigEntryValue(repoPath, name string) string { 13 | c, err := GetConfig(repoPath) 14 | if err != nil { 15 | panic(err) 16 | } 17 | return c.Get(name) 18 | } 19 | 20 | func TestGetConfigMultipleValues(t *testing.T) { 21 | localRepo, err := os.MkdirTemp("", "repo") 22 | defer os.RemoveAll(localRepo) 23 | 24 | assert.NoError(t, err, fmt.Sprintf("unable to create the local Git repo: %s", err)) 25 | 26 | cmd := commandBuilderInDir(localRepo) 27 | 28 | // init and config the local Git repo 29 | assert.NoError(t, cmd("git", "init").Run()) 30 | assert.NoError(t, cmd("git", "config", "user.email", "spokes-receive-pack@github.com").Run()) 31 | assert.NoError(t, cmd("git", "config", "user.name", "spokes-receive-pack").Run()) 32 | assert.NoError(t, cmd("git", "config", "receive.hiderefs", "refs/pull/").Run()) 33 | assert.NoError(t, cmd("git", "config", "--add", "receive.hiderefs", "refs/gh/").Run()) 34 | assert.NoError(t, cmd("git", "config", "--add", "receive.hiderefs", "refs/__gh__").Run()) 35 | 36 | config, err := GetConfig(localRepo) 37 | assert.NoError(t, err, "unable to properly extract the receive section from the GitConfig") 38 | 39 | values := config.GetAll("receive.hiderefs") 40 | assert.Equalf(t, 3, len(values), "expected %d values but got %d", 3, len(values)) 41 | assert.Equal(t, values[0], "refs/pull/") 42 | assert.Equal(t, values[1], "refs/gh/") 43 | assert.Equal(t, values[2], "refs/__gh__") 44 | } 45 | 46 | func TestGetConfigEntryValues(t *testing.T) { 47 | localRepo, err := os.MkdirTemp("", "repo") 48 | defer os.RemoveAll(localRepo) 49 | 50 | assert.NoError(t, err, fmt.Sprintf("unable to create the local Git repo: %s", err)) 51 | 52 | cmd := commandBuilderInDir(localRepo) 53 | 54 | // init and config the local Git repo 55 | assert.NoError(t, cmd("git", "init").Run()) 56 | assert.NoError(t, cmd("git", "config", "user.email", "spokes-receive-pack@github.com").Run()) 57 | assert.NoError(t, cmd("git", "config", "user.name", "spokes-receive-pack").Run()) 58 | assert.NoError(t, cmd("git", "config", "receive.fsckObjects", "true").Run()) 59 | assert.NoError(t, cmd("git", "config", "receive.maxsize", "11").Run()) 60 | 61 | fsckObjects := testGetConfigEntryValue(localRepo, "receive.fsckObjects") 62 | assert.Equal(t, "true", fsckObjects) 63 | maxSize := testGetConfigEntryValue(localRepo, "receive.maxsize") 64 | assert.Equal(t, "11", maxSize) 65 | } 66 | 67 | func TestGetConfigEntryMultipleValues(t *testing.T) { 68 | localRepo, err := os.MkdirTemp("", "repo") 69 | defer os.RemoveAll(localRepo) 70 | 71 | assert.NoError(t, err, fmt.Sprintf("unable to create the local Git repo: %s", err)) 72 | 73 | cmd := commandBuilderInDir(localRepo) 74 | 75 | // init and config the local Git repo 76 | assert.NoError(t, cmd("git", "init").Run()) 77 | assert.NoError(t, cmd("git", "config", "user.email", "spokes-receive-pack@github.com").Run()) 78 | assert.NoError(t, cmd("git", "config", "user.name", "spokes-receive-pack").Run()) 79 | assert.NoError(t, cmd("git", "config", "receive.multivalue", "a").Run()) 80 | assert.NoError(t, cmd("git", "config", "--add", "receive.multivalue", "b").Run()) 81 | assert.NoError(t, cmd("git", "config", "--add", "receive.multivalue", "c").Run()) 82 | 83 | fsckObjects := testGetConfigEntryValue(localRepo, "receive.multivalue") 84 | assert.Equal(t, "c", fsckObjects) 85 | } 86 | func TestGetPrefixParsesArgs(t *testing.T) { 87 | localRepo, err := os.MkdirTemp("", "repo") 88 | defer os.RemoveAll(localRepo) 89 | 90 | assert.NoError(t, err, fmt.Sprintf("unable to create the local Git repo: %s", err)) 91 | 92 | cmd := commandBuilderInDir(localRepo) 93 | 94 | // init and config the local Git repo 95 | assert.NoError(t, cmd("git", "init").Run()) 96 | assert.NoError(t, cmd("git", "config", "user.email", "spokes-receive-pack@github.com").Run()) 97 | assert.NoError(t, cmd("git", "config", "user.name", "spokes-receive-pack").Run()) 98 | assert.NoError(t, cmd("git", "config", "receive.fsck.missingEmail", "ignore").Run()) 99 | assert.NoError(t, cmd("git", "config", "receive.fsck.badTagName", "ignore").Run()) 100 | assert.NoError(t, cmd("git", "config","--add", "receive.fsck.badTagName", "error").Run()) 101 | 102 | config, _ := GetConfig(localRepo) 103 | prefix := config.GetPrefix("receive.fsck.") 104 | 105 | assert.Equal(t, prefix["missingemail"][0], "ignore") 106 | assert.Equal(t, prefix["badtagname"][0], "ignore") 107 | assert.Equal(t, prefix["badtagname"][1], "error") 108 | } 109 | 110 | 111 | func commandBuilderInDir(dir string) func(string, ...string) *exec.Cmd { 112 | return func(program string, args ...string) *exec.Cmd { 113 | c := exec.Command(program, args...) 114 | c.Dir = dir 115 | return c 116 | } 117 | } 118 | 119 | func TestParseSigned(t *testing.T) { 120 | for _, c := range []struct { 121 | str string 122 | want int 123 | wantErr string 124 | }{ 125 | // valid input, no suffix 126 | {"81", 81, ""}, 127 | 128 | // valid input, with lower- and upper-case suffixes 129 | {"2k", 2 * 1024, ""}, 130 | {"3m", 3 * 1024 * 1024, ""}, 131 | {"4g", 4 * 1024 * 1024 * 1024, ""}, 132 | {"2K", 2 * 1024, ""}, 133 | {"3M", 3 * 1024 * 1024, ""}, 134 | {"4G", 4 * 1024 * 1024 * 1024, ""}, 135 | 136 | // valid negative input, with lower- and upper-case suffixes 137 | {"-2k", -2 * 1024, ""}, 138 | {"-3m", -3 * 1024 * 1024, ""}, 139 | {"-4g", -4 * 1024 * 1024 * 1024, ""}, 140 | {"-2K", -2 * 1024, ""}, 141 | {"-3M", -3 * 1024 * 1024, ""}, 142 | {"-4G", -4 * 1024 * 1024 * 1024, ""}, 143 | 144 | // empty input, just a suffix 145 | {"k", 0, "strconv.Atoi: parsing \"\": invalid syntax"}, 146 | {"m", 0, "strconv.Atoi: parsing \"\": invalid syntax"}, 147 | {"g", 0, "strconv.Atoi: parsing \"\": invalid syntax"}, 148 | 149 | // invalid input, no suffix 150 | {"NaN", 0, "strconv.Atoi: parsing \"NaN\": invalid syntax"}, 151 | } { 152 | got, gotErr := ParseSigned(c.str) 153 | 154 | assert.Equal(t, c.want, got) 155 | if c.wantErr != "" { 156 | assert.EqualError(t, gotErr, c.wantErr) 157 | } 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /internal/governor/conn.go: -------------------------------------------------------------------------------- 1 | package governor 2 | 3 | import ( 4 | "bufio" 5 | "context" 6 | "net" 7 | "os" 8 | "strconv" 9 | "strings" 10 | "time" 11 | 12 | "github.com/github/spokes-receive-pack/internal/sockstat" 13 | ) 14 | 15 | const ( 16 | connectTimeout = time.Second 17 | ) 18 | 19 | func scheduleTimeout() time.Duration { 20 | timeout := time.Second 21 | if v := os.Getenv("SCHEDULE_CMD_TIMEOUT"); v != "" { 22 | if d, err := strconv.ParseInt(v, 10, 64); err == nil { 23 | timeout = time.Duration(d) * time.Millisecond 24 | } 25 | } 26 | 27 | return timeout 28 | } 29 | 30 | func shouldFailClosed() bool { 31 | return os.Getenv("FAIL_CLOSED") == "1" 32 | } 33 | 34 | // Start connects to governor and sends the "update" and "schedule" messages. 35 | // 36 | // If "schedule" says to wait, Start will pause for the specified time and try 37 | // calling "schedule" again. 38 | // 39 | // If there is a connection or other low level error when talking to governor, 40 | // Start will return (nil, nil). 41 | func Start(ctx context.Context, gitDir string) (*Conn, error) { 42 | sock, err := connect(ctx) 43 | if err != nil { 44 | return nil, nil 45 | } 46 | 47 | updateData := readSockstat(os.Environ()) 48 | updateData.PID = os.Getpid() 49 | updateData.Program = "spokes-receive-pack" 50 | updateData.GitDir = gitDir 51 | if err := update(sock, updateData); err != nil { 52 | sock.Close() 53 | return nil, nil 54 | } 55 | 56 | timeout := scheduleTimeout() 57 | failClosed := shouldFailClosed() 58 | br := bufio.NewReader(sock) 59 | for { 60 | // Give governor a limited time to respond. 61 | if err := sock.SetReadDeadline(time.Now().Add(timeout)); err != nil { 62 | break 63 | } 64 | 65 | err := schedule(br, sock) 66 | if err == nil { 67 | break 68 | } 69 | 70 | switch e := err.(type) { 71 | case WaitError: 72 | time.Sleep(e.Duration) 73 | case FailError: 74 | sock.Close() 75 | return nil, err 76 | case net.Error: 77 | sock.Close() 78 | 79 | if e.Timeout() && failClosed { 80 | return nil, err 81 | } 82 | 83 | return nil, nil 84 | default: 85 | sock.Close() 86 | return nil, nil 87 | } 88 | } 89 | 90 | return &Conn{sock: sock}, nil 91 | } 92 | 93 | // Conn is an active connection to governor. 94 | type Conn struct { 95 | sock net.Conn 96 | finish finishData 97 | } 98 | 99 | // SetError stores an error to include with the finish message. 100 | // 101 | // It is safe to call SetError with a nil *Conn. 102 | func (c *Conn) SetError(exitCode uint8, message string) { 103 | if c == nil { 104 | return 105 | } 106 | c.finish.ResultCode = exitCode 107 | c.finish.Fatal = message 108 | } 109 | 110 | // SetReceivePackSize records the incoming packfile's size to include with the 111 | // finish message. 112 | // 113 | // It is safe to call SetReceivePackSize with a nil *Conn. 114 | func (c *Conn) SetReceivePackSize(size int64) { 115 | if c == nil { 116 | return 117 | } 118 | if size > 0 { 119 | c.finish.ReceivePackSize = uint64(size) 120 | } 121 | } 122 | 123 | // Finish sends the "finish" message to governor and closes the connection. 124 | // 125 | // It is safe to call Finish with a nil *Conn. 126 | func (c *Conn) Finish(ctx context.Context) { 127 | if c == nil || c.sock == nil { 128 | return 129 | } 130 | 131 | stats := getProcStats() 132 | c.finish.CPU = stats.CPU 133 | c.finish.RSS = stats.RSS 134 | c.finish.DiskReadBytes = stats.DiskReadBytes 135 | c.finish.DiskWriteBytes = stats.DiskWriteBytes 136 | 137 | _ = finish(c.sock, c.finish) 138 | 139 | c.sock.Close() 140 | c.sock = nil 141 | } 142 | 143 | type procStats struct { 144 | CPU uint32 145 | RSS uint64 146 | DiskReadBytes uint64 147 | DiskWriteBytes uint64 148 | } 149 | 150 | func connect(ctx context.Context) (net.Conn, error) { 151 | ctx, cancel := context.WithTimeout(ctx, connectTimeout) 152 | defer cancel() 153 | 154 | path := os.Getenv("GIT_SOCKSTAT_PATH") 155 | if path == "" { 156 | path = "/run/governor/client.sock" 157 | } 158 | 159 | dialer := &net.Dialer{} 160 | return dialer.DialContext(ctx, "unix", path) 161 | } 162 | 163 | func readSockstat(environ []string) updateData { 164 | var res updateData 165 | 166 | for _, env := range environ { 167 | if !strings.HasPrefix(env, sockstat.Prefix) { 168 | continue 169 | } 170 | env = env[len(sockstat.Prefix):] 171 | parts := strings.SplitN(env, "=", 2) 172 | if len(parts) != 2 { 173 | continue 174 | } 175 | switch parts[0] { 176 | case "repo_name": 177 | res.RepoName = sockstat.StringValue(parts[1]) 178 | case "repo_id": 179 | res.RepoID = sockstat.Uint32Value(parts[1]) 180 | case "network_id": 181 | res.NetworkID = sockstat.Uint32Value(parts[1]) 182 | case "user_id": 183 | res.UserID = sockstat.Uint32Value(parts[1]) 184 | case "real_ip": 185 | res.RealIP = sockstat.StringValue(parts[1]) 186 | case "request_id": 187 | res.RequestID = sockstat.StringValue(parts[1]) 188 | case "user_agent": 189 | res.UserAgent = sockstat.StringValue(parts[1]) 190 | case "features": 191 | res.Features = sockstat.StringValue(parts[1]) 192 | case "via": 193 | res.Via = sockstat.StringValue(parts[1]) 194 | case "ssh_connection": 195 | res.SSHConnection = sockstat.StringValue(parts[1]) 196 | case "babeld": 197 | res.Babeld = sockstat.StringValue(parts[1]) 198 | case "git_protocol": 199 | res.GitProtocol = sockstat.StringValue(parts[1]) 200 | case "pubkey_verifier_id": 201 | res.PubkeyVerifierID = sockstat.Uint32Value(parts[1]) 202 | case "pubkey_creator_id": 203 | res.PubkeyCreatorID = sockstat.Uint32Value(parts[1]) 204 | case "max_delay": 205 | res.MaxDelay = sockstat.Uint32Value(parts[1]) 206 | case "command_id": 207 | res.CommandID = sockstat.StringValue(parts[1]) 208 | case "group_id": 209 | res.GroupID = sockstat.StringValue(parts[1]) 210 | case "group_leader": 211 | res.GroupLeader = sockstat.GetBool(parts[1]) 212 | case "is_importing": 213 | res.IsImporting = sockstat.BoolValue(parts[1]) 214 | case "import_skip_push_limit": 215 | res.ImportSkipPushLimit = sockstat.BoolValue(parts[1]) 216 | case "import_soft_throttling": 217 | res.ImportSoftThrottling = sockstat.BoolValue(parts[1]) 218 | } 219 | } 220 | 221 | return res 222 | } 223 | -------------------------------------------------------------------------------- /internal/governor/conn_test.go: -------------------------------------------------------------------------------- 1 | package governor 2 | 3 | import "testing" 4 | 5 | func TestReadSockstat(t *testing.T) { 6 | examples := []struct { 7 | label string 8 | environ []string 9 | expected updateData 10 | }{ 11 | { 12 | label: "ignored environment", 13 | environ: []string{ 14 | "HTTP_X_SOCKSTAT_repo_name=ignored", 15 | "REMOTE_ADDR=ignored", 16 | "GIT_SOCKSTAT_VAR_ignored=ignored", 17 | "GIT_SOCKSTAT_VAR_user_id=ignored", 18 | "GIT_SOCKSTAT_VAR_network_id=bool:false", 19 | }, 20 | }, 21 | { 22 | label: "all the fields", 23 | environ: []string{ 24 | "GIT_SOCKSTAT_VAR_repo_name=a/b", 25 | "GIT_SOCKSTAT_VAR_repo_id=uint:1", 26 | "GIT_SOCKSTAT_VAR_network_id=uint:2", 27 | "GIT_SOCKSTAT_VAR_user_id=uint:3", 28 | "GIT_SOCKSTAT_VAR_real_ip=1.2.3.4", 29 | "GIT_SOCKSTAT_VAR_request_id=AAAA:BBBB:CCCC-DDDD", 30 | "GIT_SOCKSTAT_VAR_user_agent=Testing/1.2.3 xyz=blah", 31 | "GIT_SOCKSTAT_VAR_features=random", 32 | "GIT_SOCKSTAT_VAR_via=git", 33 | "GIT_SOCKSTAT_VAR_ssh_connection=ssh-anything", 34 | "GIT_SOCKSTAT_VAR_babeld=babeld-anything", 35 | "GIT_SOCKSTAT_VAR_git_protocol=http", 36 | "GIT_SOCKSTAT_VAR_pubkey_verifier_id=uint:10", 37 | "GIT_SOCKSTAT_VAR_pubkey_creator_id=uint:11", 38 | }, 39 | expected: updateData{ 40 | RepoName: "a/b", 41 | RepoID: 1, 42 | NetworkID: 2, 43 | UserID: 3, 44 | RealIP: "1.2.3.4", 45 | RequestID: "AAAA:BBBB:CCCC-DDDD", 46 | UserAgent: "Testing/1.2.3 xyz=blah", 47 | Features: "random", 48 | Via: "git", 49 | SSHConnection: "ssh-anything", 50 | Babeld: "babeld-anything", 51 | GitProtocol: "http", 52 | PubkeyVerifierID: 10, 53 | PubkeyCreatorID: 11, 54 | }, 55 | }, 56 | } 57 | 58 | for _, ex := range examples { 59 | actual := readSockstat(ex.environ) 60 | if actual != ex.expected { 61 | t.Errorf("%s: incorrect output\nexpected: %+v\nactual: %+v\n", ex.label, ex.expected, actual) 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /internal/governor/governor.go: -------------------------------------------------------------------------------- 1 | package governor 2 | 3 | import ( 4 | "bufio" 5 | "encoding/json" 6 | "fmt" 7 | "io" 8 | "log" 9 | "strconv" 10 | "strings" 11 | "time" 12 | ) 13 | 14 | type updateData struct { 15 | // The process ID of the process being run. On Linux, this will 16 | // usually be ignored by governor in favor of the PID of the process 17 | // connecting to the socket. 18 | PID int `json:"pid,omitempty"` 19 | 20 | // An ID that identifies a group of commands that all make up one 21 | // logical request. 22 | GroupID string `json:"group_id,omitempty"` 23 | 24 | // True if this is the top-level request in a group. 25 | GroupLeader bool `json:"group_leader,omitempty"` 26 | 27 | // The quality of service for this request. If not set, governor will choose the quality of service to use (probably delayable). 28 | QualityOfService string `json:"qos,omitempty"` 29 | 30 | // The name of the program being run. 31 | Program string `json:"program,omitempty"` 32 | 33 | // The git directory path. 34 | GitDir string `json:"git_dir,omitempty"` 35 | 36 | // The repository's NWO, if available. 37 | RepoName string `json:"repo_name,omitempty"` 38 | 39 | // The repository's numerical ID. 40 | RepoID uint32 `json:"repo_id,omitempty"` 41 | 42 | // The repository's network ID. 43 | NetworkID uint32 `json:"network_id,omitempty"` 44 | 45 | // The ID of the GitHub user on whose behalf this process is being 46 | // run. 47 | UserID uint32 `json:"user_id,omitempty"` 48 | 49 | // The IP number of the user making the request. 50 | RealIP string `json:"real_ip,omitempty"` 51 | 52 | // The request ID of the request that triggered this process. 53 | RequestID string `json:"request_id,omitempty"` 54 | 55 | // The User-Agent from the request. For Spokes API requests, this is 56 | // the internal client's User-Agent with a spokesd version appended to 57 | // it. 58 | UserAgent string `json:"user_agent,omitempty"` 59 | 60 | // The X-Spokesd-TLS-Client header from the request. On dotcom, this is 61 | // taken from the CN of the client's certificate. In other 62 | // environments, this will not be set. 63 | ClientApp string `json:"client_app,omitempty"` 64 | 65 | Features string `json:"features,omitempty"` 66 | Via string `json:"via,omitempty"` 67 | SSHConnection string `json:"ssh_connection,omitempty"` 68 | Babeld string `json:"babeld,omitempty"` 69 | GitProtocol string `json:"git_protocol,omitempty"` 70 | PubkeyVerifierID uint32 `json:"pubkey_verifier_id,omitempty"` 71 | PubkeyCreatorID uint32 `json:"pubkey_creator_id,omitempty"` 72 | MaxDelay uint32 `json:"max_delay,omitempty"` 73 | // An ID that identifies a group of commands that all make up one 74 | // logical request. Is only used by the githttpdaemon to sync 75 | // its gitmon proxy and request scheduler logical threads 76 | CommandID string `json:"command_id,omitempty"` 77 | // IsImporting is true if the command is an import. 78 | IsImporting bool `json:"is_importing,omitempty"` 79 | // ImportSkipPushLimit is true if the command is an import and 80 | // we want to skip the push limit for a command. 81 | ImportSkipPushLimit bool `json:"import_skip_push_limit,omitempty"` 82 | // ImportSoftThrottling is true if the command is an import and 83 | // we want to apply it some soft throttling policies. 84 | ImportSoftThrottling bool `json:"import_soft_throttling,omitempty"` 85 | } 86 | 87 | func update(w io.Writer, ud updateData) error { 88 | updateMsg := struct { 89 | Command string `json:"command"` 90 | Data updateData `json:"data"` 91 | }{ 92 | Command: "update", 93 | Data: ud, 94 | } 95 | 96 | msg, err := json.Marshal(updateMsg) 97 | if err != nil { 98 | return err 99 | } 100 | 101 | _, err = w.Write(msg) 102 | return err 103 | } 104 | 105 | type WaitError struct { 106 | Duration time.Duration 107 | Reason string 108 | } 109 | 110 | func newWaitError(duration time.Duration, reason string) error { 111 | return WaitError{ 112 | Duration: duration, 113 | Reason: reason, 114 | } 115 | } 116 | 117 | func (err WaitError) Error() string { 118 | return fmt.Sprintf("governor asked us to wait %s: %s", err.Duration, err.Reason) 119 | } 120 | 121 | type FailError struct { 122 | Reason string 123 | } 124 | 125 | func newFailError(reason string) error { 126 | return FailError{ 127 | Reason: reason, 128 | } 129 | } 130 | 131 | func (err FailError) Error() string { 132 | return fmt.Sprintf("governor refuses to schedule us: %s", err.Reason) 133 | } 134 | 135 | func schedule(r *bufio.Reader, w io.Writer) error { 136 | const msg = `{"command":"schedule"}` 137 | 138 | _, err := w.Write([]byte(msg)) 139 | if err != nil { 140 | return err 141 | } 142 | 143 | b, err := r.ReadBytes('\n') 144 | if err != nil { 145 | return err 146 | } 147 | 148 | line := string(b[:len(b)-1]) 149 | 150 | words := strings.SplitN(line, " ", 3) 151 | switch words[0] { 152 | case "continue": 153 | return nil 154 | case "wait": 155 | duration := 1 * time.Second 156 | reason := "UNKNOWN" 157 | if len(words) > 1 { 158 | d, err := strconv.Atoi(words[1]) 159 | if err != nil { 160 | log.Printf("warning: 'wait' duration %q could not be parsed", words[1]) 161 | } else { 162 | duration = time.Duration(d) * time.Second 163 | } 164 | } 165 | if len(words) > 2 { 166 | reason = strings.Join(words[2:], " ") 167 | } 168 | return newWaitError(duration, reason) 169 | case "fail": 170 | reason := "UNKNOWN" 171 | if len(words) > 1 { 172 | reason = strings.Join(words[1:], " ") 173 | } 174 | return newFailError(reason) 175 | default: 176 | return fmt.Errorf("unexpected response %q from governor", line) 177 | } 178 | } 179 | 180 | type finishData struct { 181 | // The command's result code. 182 | ResultCode uint8 `json:"result_code"` 183 | 184 | // The amount of user plus system CPU used by the command, as an 185 | // integer number of milliseconds. 186 | // 187 | // If this is the GroupLeader, this is an aggregate value for the whole 188 | // group. 189 | CPU uint32 `json:"cpu,omitempty"` 190 | 191 | // The number of times that the filesystem had to perform input. 192 | // (Actually, git sends `ru_inblock`, which is the number of times 193 | // that the filesystem had to perform input). 194 | // 195 | // If this is the GroupLeader, this is an aggregate value for the whole 196 | // group. 197 | DiskReadBytes uint64 `json:"disk_read_bytes,omitempty"` 198 | 199 | // The number of bytes written to the filesystem. (Actually, git 200 | // sends `ru_outblock`, which is the number of times that the 201 | // filesystem had to perform output). 202 | // 203 | // If this is the GroupLeader, this is an aggregate value for the whole 204 | // group. 205 | DiskWriteBytes uint64 `json:"disk_write_bytes,omitempty"` 206 | 207 | // The maximum resident set size. 208 | // 209 | // If this is the GroupLeader, this is an aggregate value for the whole 210 | // group. 211 | RSS uint64 `json:"rss,omitempty"` 212 | 213 | // The size of the uploaded packfile, in bytes (implemented only 214 | // for `upload-pack`). 215 | // 216 | // If this is the GroupLeader, this is an aggregate value for the whole 217 | // group. 218 | UploadedBytes uint64 `json:"uploaded_bytes,omitempty"` 219 | 220 | // The size of the received packfile, in bytes (implemented only 221 | // for `receive-pack`). 222 | // 223 | // If this is the GroupLeader, this is an aggregate value for the whole 224 | // group. 225 | ReceivePackSize uint64 `json:"receive_pack_size,omitempty"` 226 | 227 | // Bitwise OR of: 228 | // 229 | // * 0x01 — Was this invocation of `upload-pack` a clone (as 230 | // opposed to a fetch)? 231 | // 232 | // * 0x02 — Was it a shallow (as opposed to a full) 233 | // clone/fetch? 234 | Cloning uint8 `json:"cloning,omitempty"` 235 | 236 | // If git died, what was the error message that it emitted? 237 | Fatal string `json:"fatal,omitempty"` 238 | } 239 | 240 | func finish(w io.Writer, fd finishData) error { 241 | finishMsg := struct { 242 | Command string `json:"command"` 243 | Data finishData `json:"data"` 244 | }{ 245 | Command: "finish", 246 | Data: fd, 247 | } 248 | 249 | msg, err := json.Marshal(finishMsg) 250 | if err != nil { 251 | return err 252 | } 253 | 254 | _, err = w.Write(msg) 255 | return err 256 | } 257 | -------------------------------------------------------------------------------- /internal/governor/governor_test.go: -------------------------------------------------------------------------------- 1 | package governor 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "errors" 7 | "io" 8 | "strings" 9 | "testing" 10 | "time" 11 | 12 | "github.com/stretchr/testify/assert" 13 | ) 14 | 15 | func TestUpdate(t *testing.T) { 16 | var buf bytes.Buffer 17 | 18 | err := update(&buf, updateData{Program: "test-prog"}) 19 | 20 | assert.NoError(t, err) 21 | assert.Equal(t, `{"command":"update","data":{"program":"test-prog"}}`, buf.String()) 22 | } 23 | 24 | func TestSchedule(t *testing.T) { 25 | examples := []struct { 26 | response string 27 | expectedError error 28 | }{ 29 | { 30 | response: "continue\n", 31 | }, 32 | { 33 | response: "wait 100\n", 34 | expectedError: WaitError{Duration: 100 * time.Second, Reason: "UNKNOWN"}, 35 | }, 36 | { 37 | response: "wait 100 network:ip\n", 38 | expectedError: WaitError{Duration: 100 * time.Second, Reason: "network:ip"}, 39 | }, 40 | { 41 | response: "wait 100 network:reason with spaces\n", 42 | expectedError: WaitError{Duration: 100 * time.Second, Reason: "network:reason with spaces"}, 43 | }, 44 | { 45 | response: "fail Too Busy\n", 46 | expectedError: FailError{Reason: "Too Busy"}, 47 | }, 48 | { 49 | response: "", 50 | expectedError: io.EOF, 51 | }, 52 | { 53 | response: "\n", 54 | expectedError: errors.New(`unexpected response "" from governor`), 55 | }, 56 | } 57 | 58 | for _, ex := range examples { 59 | t.Run(strings.TrimSpace(ex.response), func(t *testing.T) { 60 | var toGov bytes.Buffer 61 | fromGov := bufio.NewReader(strings.NewReader(ex.response)) 62 | 63 | err := schedule(fromGov, &toGov) 64 | 65 | assert.Equal(t, ex.expectedError, err) 66 | assert.Equal(t, `{"command":"schedule"}`, toGov.String()) 67 | }) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /internal/governor/procstats.go: -------------------------------------------------------------------------------- 1 | //go:build !linux 2 | 3 | package governor 4 | 5 | import "syscall" 6 | 7 | func getProcStats() procStats { 8 | var ru syscall.Rusage 9 | if err := syscall.Getrusage(syscall.RUSAGE_SELF, &ru); err != nil { 10 | return procStats{} 11 | } 12 | return procStats{ 13 | CPU: uint32(ru.Utime.Sec*1000) + uint32(ru.Utime.Usec/1000) + uint32(ru.Stime.Sec*1000) + uint32(ru.Stime.Usec/1000), 14 | RSS: uint64(ru.Maxrss), 15 | DiskReadBytes: uint64(ru.Inblock), 16 | DiskWriteBytes: uint64(ru.Oublock), 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /internal/governor/procstats_linux.go: -------------------------------------------------------------------------------- 1 | //go:build linux 2 | 3 | package governor 4 | 5 | import ( 6 | "bytes" 7 | "os" 8 | "strconv" 9 | "strings" 10 | "syscall" 11 | ) 12 | 13 | func getProcStats() procStats { 14 | var res procStats 15 | 16 | var ru syscall.Rusage 17 | if err := syscall.Getrusage(syscall.RUSAGE_SELF, &ru); err == nil { 18 | res.CPU = uint32(ru.Utime.Sec*1000) + uint32(ru.Utime.Usec/1000) + uint32(ru.Stime.Sec*1000) + uint32(ru.Stime.Usec/1000) 19 | } 20 | 21 | res.RSS = getPeakRSS() 22 | 23 | if res.RSS == 0 { 24 | pageSize := syscall.Getpagesize() 25 | if pageSize > 0 { 26 | stat, err := os.ReadFile("/proc/self/stat") 27 | if err == nil { 28 | fields := bytes.Fields(stat) 29 | if len(fields) > 23 { 30 | val, err := strconv.ParseUint(string(fields[23]), 10, 64) 31 | if err == nil { 32 | res.RSS = val * uint64(pageSize) 33 | } 34 | } 35 | } 36 | } 37 | } 38 | 39 | iostats, err := os.ReadFile("/proc/self/io") 40 | if err == nil { 41 | const ( 42 | readPrefix = "read_bytes: " 43 | writePrefix = "write_bytes: " 44 | cancelledWritePrefix = "cancelled_write_bytes: " 45 | ) 46 | for _, line := range strings.Split(string(iostats), "\n") { 47 | switch { 48 | case strings.HasPrefix(line, readPrefix): 49 | if val, err := strconv.ParseUint(line[len(readPrefix):], 10, 64); err != nil { 50 | res.DiskReadBytes = val 51 | } 52 | case strings.HasPrefix(line, writePrefix): 53 | if val, err := strconv.ParseUint(line[len(writePrefix):], 10, 64); err != nil { 54 | res.DiskWriteBytes = val 55 | } 56 | case strings.HasPrefix(line, cancelledWritePrefix): 57 | if val, err := strconv.ParseUint(line[len(cancelledWritePrefix):], 10, 64); err != nil { 58 | // This always comes after write_bytes. 59 | if val > res.DiskWriteBytes { 60 | res.DiskWriteBytes = 0 61 | } else { 62 | res.DiskWriteBytes -= val 63 | } 64 | } 65 | } 66 | } 67 | } 68 | 69 | return res 70 | } 71 | 72 | func getPeakRSS() uint64 { 73 | const prefix = "\nVmHWM:" 74 | 75 | stat, err := os.ReadFile("/proc/self/status") 76 | if err != nil { 77 | return 0 78 | } 79 | 80 | i := bytes.Index(stat, []byte(prefix)) 81 | if i == -1 { 82 | return 0 83 | } 84 | 85 | val, err := strconv.ParseUint(string(stat[i:]), 10, 64) 86 | if err != nil { 87 | return 0 88 | } 89 | 90 | return val * 1024 91 | } 92 | -------------------------------------------------------------------------------- /internal/integration/capabilities_test.go: -------------------------------------------------------------------------------- 1 | //go:build integration 2 | 3 | package integration 4 | 5 | import ( 6 | "bufio" 7 | "context" 8 | "os" 9 | "os/exec" 10 | "path/filepath" 11 | "testing" 12 | "time" 13 | 14 | "github.com/github/spokes-receive-pack/internal/pktline" 15 | "github.com/stretchr/testify/assert" 16 | "github.com/stretchr/testify/require" 17 | ) 18 | 19 | func TestCapabilities(t *testing.T) { 20 | const ( 21 | defaultBranch = "refs/heads/main" 22 | transferhide1 = "refs/__transferhiderefs1/example/anything" 23 | transferhide2 = "refs/__transferhiderefs2/example/anything" 24 | transferunhide = "refs/__transferhiderefs2/exception/anything" // nested inside of __transferhiderefs2 25 | receivehide1 = "refs/__receivehiderefs1/example/anything" 26 | receivehide2 = "refs/__receivehiderefs2/example/anything" 27 | uploadhide = "refs/__uploadhiderefs/example/anything" 28 | 29 | createBranch = "refs/heads/newbranch" 30 | createtransferhide1 = "refs/__transferhiderefs1/example/new" 31 | createtransferhide2 = "refs/__transferhiderefs2/example/new" 32 | createtransferunhide = "refs/__transferhiderefs2/exception/new" // nested inside of __transferhiderefs2 33 | createreceivehide1 = "refs/__receivehiderefs1/example/new" 34 | createreceivehide2 = "refs/__receivehiderefs2/example/new" 35 | createuploadhide = "refs/__uploadhiderefs/example/new" 36 | 37 | // This needs to be reachable from refs/heads/main 38 | testCommit = "e589bdee50e39beac56220c4b7a716225f79e3cf" 39 | 40 | gitConfigParameters = `'transfer.hideRefs=refs/__transferhiderefs2' ` + 41 | `'transfer.hideRefs='\!'refs/__transferhiderefs2/exception' ` + 42 | `'receive.hideRefs=refs/__receivehiderefs2'` 43 | ) 44 | 45 | wd, err := os.Getwd() 46 | require.NoError(t, err) 47 | origin := filepath.Join(wd, "testdata/remote/git-internals-fork.git") 48 | 49 | testRepo := t.TempDir() 50 | requireRun(t, "git", "init", "--bare", testRepo) 51 | requireRun(t, "git", "-C", testRepo, "fetch", origin, defaultBranch+":"+defaultBranch) 52 | 53 | getCapsAdv := func(ctx context.Context, t *testing.T, push_options bool, extraEnv ...string) pktline.Capabilities { 54 | srp := exec.CommandContext(ctx, "spokes-receive-pack", "--stateless-rpc", "--advertise-refs", ".") 55 | srp.Dir = testRepo 56 | configParams := gitConfigParameters 57 | if push_options { 58 | configParams = gitConfigParameters + ` 'receive.advertisePushOptions=true'` 59 | } 60 | 61 | srp.Env = append(os.Environ(), 62 | "GIT_CONFIG_PARAMETERS="+configParams, 63 | "GIT_SOCKSTAT_VAR_spokes_quarantine=bool:true", 64 | "GIT_SOCKSTAT_VAR_quarantine_id=config-test-quarantine-id") 65 | srp.Env = append(srp.Env, extraEnv...) 66 | srp.Stderr = &testLogWriter{t} 67 | srpOut, err := srp.StdoutPipe() 68 | require.NoError(t, err) 69 | srp.Start() 70 | 71 | bufSRPOut := bufio.NewReader(srpOut) 72 | _, capsLine, err := readAdv(bufSRPOut) 73 | require.NoError(t, err) 74 | caps, err := pktline.ParseCapabilities([]byte(capsLine)) 75 | require.NoError(t, err) 76 | 77 | assert.NoError(t, srp.Wait()) 78 | 79 | return caps 80 | } 81 | 82 | t.Run("with request id", func(t *testing.T) { 83 | ctx, cancel := context.WithTimeout(context.Background(), time.Second) 84 | defer cancel() 85 | 86 | caps := getCapsAdv(ctx, t, false, "GIT_SOCKSTAT_VAR_request_id=test:request:id") 87 | 88 | assert.Equal(t, []string{ 89 | "agent", 90 | "atomic", 91 | "delete-refs", 92 | "object-format", 93 | "ofs-delta", 94 | "quiet", 95 | "report-status", 96 | "report-status-v2", 97 | "session-id", 98 | "side-band-64k", 99 | }, caps.Names()) 100 | 101 | assert.Equal(t, caps.SessionId().Value(), "test:request:id") 102 | assert.Equal(t, caps.ObjectFormat().Value(), "sha1") 103 | assert.Regexp(t, "^github/spokes-receive-pack-[0-9a-f]+$", caps.Agent().Value()) 104 | }) 105 | 106 | t.Run("without request id", func(t *testing.T) { 107 | ctx, cancel := context.WithTimeout(context.Background(), time.Second) 108 | defer cancel() 109 | 110 | caps := getCapsAdv(ctx, t, false) 111 | 112 | assert.Equal(t, []string{ 113 | "agent", 114 | "atomic", 115 | "delete-refs", 116 | "object-format", 117 | "ofs-delta", 118 | "quiet", 119 | "report-status", 120 | "report-status-v2", 121 | "side-band-64k", 122 | }, caps.Names()) 123 | 124 | assert.Equal(t, caps.ObjectFormat().Value(), "sha1") 125 | assert.Regexp(t, "^github/spokes-receive-pack-[0-9a-f]+$", caps.Agent().Value()) 126 | }) 127 | 128 | t.Run("with push options", func(t *testing.T) { 129 | ctx, cancel := context.WithTimeout(context.Background(), time.Second) 130 | defer cancel() 131 | 132 | caps := getCapsAdv(ctx, t, true) 133 | 134 | assert.Equal(t, []string{ 135 | "agent", 136 | "atomic", 137 | "delete-refs", 138 | "object-format", 139 | "ofs-delta", 140 | "push-options", 141 | "quiet", 142 | "report-status", 143 | "report-status-v2", 144 | "side-band-64k", 145 | }, caps.Names()) 146 | 147 | assert.Equal(t, "push-options", caps.PushOptions().Name()) 148 | }) 149 | } 150 | -------------------------------------------------------------------------------- /internal/integration/chdir.go: -------------------------------------------------------------------------------- 1 | //go:build integration 2 | 3 | package integration 4 | 5 | import ( 6 | "os" 7 | "testing" 8 | ) 9 | 10 | // chdir calls os.Chdir and registers a cleanup func to change back to the original directory. 11 | // This is a test helper that should be used in place of os.Chdir in tests. 12 | func chdir(t *testing.T, dir string) error { 13 | wd, err := os.Getwd() 14 | if err != nil { 15 | return err 16 | } 17 | 18 | t.Logf("chdir %q", dir) 19 | if err := os.Chdir(dir); err != nil { 20 | return err 21 | } 22 | 23 | t.Cleanup(func() { 24 | // If this fails, it might be because it's changing back to a 25 | // tmpdir that got removed. In that case, we'll just log it, 26 | // and trust that there's another cleanup func ready to change 27 | // back to the original working dir. 28 | t.Logf("cleanup chdir %q", wd) 29 | if err := os.Chdir(wd); err != nil { 30 | t.Logf("error calling chdir(%q) during cleanup: %v", wd, err) 31 | } 32 | }) 33 | 34 | return nil 35 | } 36 | -------------------------------------------------------------------------------- /internal/integration/hiderefs_test.go: -------------------------------------------------------------------------------- 1 | //go:build integration 2 | 3 | package integration 4 | 5 | import ( 6 | "bufio" 7 | "context" 8 | "fmt" 9 | "io" 10 | "os" 11 | "os/exec" 12 | "path/filepath" 13 | "testing" 14 | "time" 15 | 16 | "github.com/stretchr/testify/assert" 17 | "github.com/stretchr/testify/require" 18 | ) 19 | 20 | func TestHiderefsConfig(t *testing.T) { 21 | const ( 22 | defaultBranch = "refs/heads/main" 23 | transferhide1 = "refs/__transferhiderefs1/example/anything" 24 | transferhide2 = "refs/__transferhiderefs2/example/anything" 25 | transferunhide = "refs/__transferhiderefs2/exception/anything" // nested inside of __transferhiderefs2 26 | receivehide1 = "refs/__receivehiderefs1/example/anything" 27 | receivehide2 = "refs/__receivehiderefs2/example/anything" 28 | uploadhide = "refs/__uploadhiderefs/example/anything" 29 | 30 | createBranch = "refs/heads/newbranch" 31 | createtransferhide1 = "refs/__transferhiderefs1/example/new" 32 | createtransferhide2 = "refs/__transferhiderefs2/example/new" 33 | createtransferunhide = "refs/__transferhiderefs2/exception/new" // nested inside of __transferhiderefs2 34 | createreceivehide1 = "refs/__receivehiderefs1/example/new" 35 | createreceivehide2 = "refs/__receivehiderefs2/example/new" 36 | createuploadhide = "refs/__uploadhiderefs/example/new" 37 | 38 | // This needs to be reachable from refs/heads/main 39 | testCommit = "e589bdee50e39beac56220c4b7a716225f79e3cf" 40 | 41 | gitConfigParameters = `'transfer.hideRefs=refs/__transferhiderefs2' ` + 42 | `'transfer.hideRefs='\!'refs/__transferhiderefs2/exception' ` + 43 | `'receive.hideRefs=refs/__receivehiderefs2'` 44 | ) 45 | 46 | wd, err := os.Getwd() 47 | require.NoError(t, err) 48 | origin := filepath.Join(wd, "testdata/remote/git-internals-fork.git") 49 | 50 | testRepo := t.TempDir() 51 | requireRun(t, "git", "init", "--bare", testRepo) 52 | requireRun(t, "git", "-C", testRepo, "fetch", origin, defaultBranch+":"+defaultBranch) 53 | requireRun(t, "git", "-C", testRepo, "update-ref", transferhide1, testCommit) 54 | requireRun(t, "git", "-C", testRepo, "update-ref", transferhide2, testCommit) 55 | requireRun(t, "git", "-C", testRepo, "update-ref", transferunhide, testCommit) 56 | requireRun(t, "git", "-C", testRepo, "update-ref", receivehide1, testCommit) 57 | requireRun(t, "git", "-C", testRepo, "update-ref", receivehide2, testCommit) 58 | requireRun(t, "git", "-C", testRepo, "update-ref", uploadhide, testCommit) 59 | requireRun(t, "git", "-C", testRepo, "config", "transfer.hiderefs", "refs/__transferhiderefs1") 60 | requireRun(t, "git", "-C", testRepo, "config", "receive.hiderefs", "refs/__receivehiderefs1") 61 | requireRun(t, "git", "-C", testRepo, "config", "uploadpack.hiderefs", "refs/__uploadhiderefs") 62 | 63 | ctx, cancel := context.WithTimeout(context.Background(), time.Second) 64 | defer cancel() 65 | 66 | srp := exec.CommandContext(ctx, "spokes-receive-pack", ".") 67 | srp.Dir = testRepo 68 | srp.Env = append(os.Environ(), 69 | "GIT_CONFIG_PARAMETERS="+gitConfigParameters, 70 | "GIT_SOCKSTAT_VAR_spokes_quarantine=bool:true", 71 | "GIT_SOCKSTAT_VAR_quarantine_id=config-test-quarantine-id") 72 | srp.Stderr = &testLogWriter{t} 73 | srpIn, err := srp.StdinPipe() 74 | require.NoError(t, err) 75 | srpOut, err := srp.StdoutPipe() 76 | require.NoError(t, err) 77 | 78 | srpErr := make(chan error) 79 | go func() { srpErr <- srp.Run() }() 80 | 81 | bufSRPOut := bufio.NewReader(srpOut) 82 | 83 | refs, _, err := readAdv(bufSRPOut) 84 | require.NoError(t, err) 85 | assert.Contains(t, refs, defaultBranch) 86 | assert.NotContains(t, refs, transferhide1) 87 | assert.NotContains(t, refs, transferhide2) 88 | assert.Contains(t, refs, transferunhide) 89 | assert.NotContains(t, refs, receivehide1) 90 | assert.NotContains(t, refs, receivehide2) 91 | assert.Contains(t, refs, uploadhide) 92 | 93 | oldnew := fmt.Sprintf("%040d %s", 0, testCommit) 94 | require.NoError(t, writePktlinef(srpIn, 95 | "%s %s\x00report-status report-status-v2 side-band-64k object-format=sha1\n", oldnew, createBranch)) 96 | require.NoError(t, writePktlinef(srpIn, 97 | "%s %s\n", oldnew, createtransferhide1)) 98 | require.NoError(t, writePktlinef(srpIn, 99 | "%s %s\n", oldnew, createtransferhide2)) 100 | require.NoError(t, writePktlinef(srpIn, 101 | "%s %s\n", oldnew, createtransferunhide)) 102 | require.NoError(t, writePktlinef(srpIn, 103 | "%s %s\n", oldnew, createreceivehide1)) 104 | require.NoError(t, writePktlinef(srpIn, 105 | "%s %s\n", oldnew, createreceivehide2)) 106 | require.NoError(t, writePktlinef(srpIn, 107 | "%s %s\n", oldnew, createuploadhide)) 108 | _, err = srpIn.Write([]byte("0000")) 109 | require.NoError(t, err) 110 | 111 | // Send an empty pack, since we're using commits that are already in 112 | // the repo. 113 | pack, err := os.Open("testdata/empty.pack") 114 | require.NoError(t, err) 115 | if _, err := io.Copy(srpIn, pack); err != nil { 116 | t.Logf("error writing pack to spokes-receive-pack input: %v", err) 117 | } 118 | 119 | refStatus, unpackRes, _, err := readResult(t, bufSRPOut) 120 | require.NoError(t, err) 121 | assert.Equal(t, map[string]string{ 122 | createBranch: "ok", 123 | createtransferhide1: "ng deny updating a hidden ref", 124 | createtransferhide2: "ng deny updating a hidden ref", 125 | createtransferunhide: "ok", 126 | createreceivehide1: "ng deny updating a hidden ref", 127 | createreceivehide2: "ng deny updating a hidden ref", 128 | createuploadhide: "ok", 129 | }, refStatus) 130 | assert.Equal(t, "unpack ok\n", unpackRes) 131 | } 132 | -------------------------------------------------------------------------------- /internal/integration/integration_networked_repo_test.go: -------------------------------------------------------------------------------- 1 | //go:build integration 2 | 3 | package integration 4 | 5 | import ( 6 | "fmt" 7 | "os" 8 | "os/exec" 9 | "path/filepath" 10 | "testing" 11 | 12 | "github.com/stretchr/testify/assert" 13 | "github.com/stretchr/testify/require" 14 | "github.com/stretchr/testify/suite" 15 | ) 16 | 17 | type SpokesReceivePackNetworkedTestSuite struct { 18 | suite.Suite 19 | clone string 20 | } 21 | 22 | func (suite *SpokesReceivePackNetworkedTestSuite) SetupTest() { 23 | req := require.New(suite.T()) 24 | 25 | // create a local clone of the fork git-internals-fork available in the network 26 | clone := "git-internals-fork" 27 | req.NoError(exec.Command("git", "clone", "testdata/remote/git-internals-fork.git", clone).Run()) 28 | 29 | // go into the clone 30 | wd, _ := os.Getwd() 31 | req.NoError(chdir(suite.T(), clone), "unable to chdir from %s into the recently cloned repo at %s", wd, clone) 32 | // init and config the local Git repo 33 | req.NoError(exec.Command("git", "init").Run()) 34 | req.NoError(exec.Command("git", "config", "user.email", "spokes-receive-pack@github.com").Run()) 35 | req.NoError(exec.Command("git", "config", "user.name", "spokes-receive-pack").Run()) 36 | 37 | // add some extra content in different branches 38 | branches := []string{"branch-in-fork-1", "branch-in-fork-2", "branch-in-fork-3"} 39 | for i, branch := range branches { 40 | req.NoError(exec.Command("git", "checkout", "-b", branch).Run()) 41 | name := fmt.Sprintf("file-%d.txt", i) 42 | req.NoErrorf( 43 | os.WriteFile(name, []byte(fmt.Sprintf("A test file with name %s", name)), 0644), 44 | "unable to create %s file in the Git repo", name) 45 | req.NoError(exec.Command("git", "add", ".").Run()) 46 | req.NoError(exec.Command("git", "commit", "--message", fmt.Sprintf("Commit %d", i)).Run()) 47 | } 48 | 49 | cloneAbsPath, err := filepath.Abs(".") 50 | req.NoError(err, "unable to compute the absolute path of our cloned repo at %s", clone) 51 | suite.clone = cloneAbsPath 52 | } 53 | 54 | func (suite *SpokesReceivePackNetworkedTestSuite) TearDownTest() { 55 | require := require.New(suite.T()) 56 | 57 | // Clean the environment before exiting 58 | require.NoError(os.RemoveAll(suite.clone)) 59 | require.NoError(os.RemoveAll("../testdata/remote/git-internals-fork.git/objects/quarantine")) 60 | } 61 | 62 | func (suite *SpokesReceivePackNetworkedTestSuite) TestSpokesReceivePackPushFork() { 63 | assert.NoError(suite.T(), chdir(suite.T(), suite.clone), "unable to chdir into our local clone of a fork at %s", suite.clone) 64 | assert.NoError( 65 | suite.T(), 66 | exec.Command( 67 | "git", "push", "--all", "--receive-pack=spokes-receive-pack-networked-wrapper", "origin").Run(), 68 | "unexpected error running the networked push with the custom spokes-receive-pack program") 69 | } 70 | 71 | func TestSpokesReceivePackNetworkedTestSuite(t *testing.T) { 72 | suite.Run(t, new(SpokesReceivePackNetworkedTestSuite)) 73 | } 74 | -------------------------------------------------------------------------------- /internal/integration/logshim.go: -------------------------------------------------------------------------------- 1 | //go:build integration 2 | 3 | package integration 4 | 5 | import "testing" 6 | 7 | type testLogWriter struct { 8 | t *testing.T 9 | } 10 | 11 | func (w *testLogWriter) Write(data []byte) (int, error) { 12 | w.t.Logf("%s", data) 13 | return len(data), nil 14 | } 15 | -------------------------------------------------------------------------------- /internal/integration/missingobjects_test.go: -------------------------------------------------------------------------------- 1 | //go:build integration 2 | 3 | package integration 4 | 5 | import ( 6 | "bufio" 7 | "context" 8 | "encoding/json" 9 | "io" 10 | "os" 11 | "os/exec" 12 | "path/filepath" 13 | "testing" 14 | "time" 15 | 16 | "github.com/github/spokes-receive-pack/internal/objectformat" 17 | "github.com/stretchr/testify/assert" 18 | "github.com/stretchr/testify/require" 19 | ) 20 | 21 | func TestMissingObjects(t *testing.T) { 22 | 23 | x := setUpMissingObjectsTestRepo(t) 24 | testRepo := x.TestRepo 25 | info := x.Info 26 | 27 | ctx, cancel := context.WithTimeout(context.Background(), time.Second) 28 | defer cancel() 29 | 30 | srp := startSpokesReceivePack(ctx, t, testRepo) 31 | 32 | refs, _, err := readAdv(srp.Out) 33 | require.NoError(t, err) 34 | assert.Equal(t, refs, map[string]string{ 35 | info.Ref: info.OldOID, 36 | info.DelRef: info.OldOID, 37 | }) 38 | 39 | // Send the pack that's missing a commit. 40 | pack, err := os.Open("testdata/missing-objects/bad.pack") 41 | require.NoError(t, err) 42 | defer pack.Close() 43 | 44 | writePushData( 45 | t, srp, 46 | []refUpdate{ 47 | // Try to update the ref that's already there to commit C (but we won't 48 | // push its parent and the remote doesn't have the parent either). 49 | {info.OldOID, info.NewOID, info.Ref}, 50 | }, 51 | pack, 52 | ) 53 | 54 | refStatus, unpackRes, _, err := readResult(t, srp.Out) 55 | require.NoError(t, err) 56 | assert.Equal(t, map[string]string{ 57 | info.Ref: "ng error processing packfiles: exit status 128", 58 | }, refStatus) 59 | assert.Equal(t, "unpack index-pack failed\n", unpackRes) 60 | } 61 | 62 | func TestDeleteAndUpdate(t *testing.T) { 63 | const refToCreate = "refs/heads/new-branch" 64 | 65 | x := setUpMissingObjectsTestRepo(t) 66 | testRepo := x.TestRepo 67 | info := x.Info 68 | 69 | ctx, cancel := context.WithTimeout(context.Background(), time.Second) 70 | defer cancel() 71 | 72 | srp := startSpokesReceivePack(ctx, t, testRepo) 73 | 74 | refs, _, err := readAdv(srp.Out) 75 | require.NoError(t, err) 76 | assert.Equal(t, refs, map[string]string{ 77 | info.Ref: info.OldOID, 78 | info.DelRef: info.OldOID, 79 | }) 80 | 81 | // Send the pack that's missing a commit. 82 | pack, err := os.Open("testdata/missing-objects/empty.pack") 83 | require.NoError(t, err) 84 | defer pack.Close() 85 | 86 | writePushData( 87 | t, srp, 88 | []refUpdate{ 89 | // Try to create another ref with a commit that the remote already has. 90 | {objectformat.NullOIDSHA1, info.OldOID, refToCreate}, 91 | // Try to delete a ref. 92 | {info.OldOID, objectformat.NullOIDSHA1, info.DelRef}, 93 | }, 94 | pack, 95 | ) 96 | 97 | refStatus, unpackRes, _, err := readResult(t, srp.Out) 98 | require.NoError(t, err) 99 | assert.Equal(t, map[string]string{ 100 | refToCreate: "ok", 101 | info.DelRef: "ok", 102 | }, refStatus) 103 | assert.Equal(t, "unpack ok\n", unpackRes) 104 | } 105 | 106 | type missingObjectsTestInfo struct { 107 | TestRepo string 108 | Info struct { 109 | OldOID string `json:"push_from"` 110 | NewOID string `json:"push_to"` 111 | Ref string `json:"ref"` 112 | DelRef string `json:"extra_ref"` 113 | } 114 | } 115 | 116 | func setUpMissingObjectsTestRepo(t *testing.T) missingObjectsTestInfo { 117 | const ( 118 | remote = "testdata/missing-objects/remote.git" 119 | badPack = "testdata/missing-objects/bad.pack" 120 | infoFile = "testdata/missing-objects/info.json" 121 | ) 122 | 123 | var res missingObjectsTestInfo 124 | 125 | infoJSON, err := os.ReadFile(infoFile) 126 | require.NoError(t, err) 127 | require.NoError(t, json.Unmarshal(infoJSON, &res.Info)) 128 | 129 | origin, err := filepath.Abs(remote) 130 | require.NoError(t, err) 131 | 132 | res.TestRepo = t.TempDir() 133 | requireRun(t, "git", "clone", "--mirror", origin, res.TestRepo) 134 | 135 | return res 136 | } 137 | 138 | type spokesReceivePackProcess struct { 139 | Cmd *exec.Cmd 140 | In io.WriteCloser 141 | Out io.Reader 142 | Err chan error 143 | } 144 | 145 | func startSpokesReceivePack(ctx context.Context, t *testing.T, testRepo string) spokesReceivePackProcess { 146 | srp := exec.CommandContext(ctx, "spokes-receive-pack", ".") 147 | srp.Dir = testRepo 148 | srp.Env = append(os.Environ(), 149 | "GIT_SOCKSTAT_VAR_spokes_quarantine=bool:true", 150 | "GIT_SOCKSTAT_VAR_quarantine_id=config-test-quarantine-id") 151 | srp.Stderr = &testLogWriter{t} 152 | srpIn, err := srp.StdinPipe() 153 | require.NoError(t, err) 154 | srpOut, err := srp.StdoutPipe() 155 | require.NoError(t, err) 156 | 157 | srpErr := make(chan error) 158 | go func() { srpErr <- srp.Run() }() 159 | 160 | bufSRPOut := bufio.NewReader(srpOut) 161 | 162 | return spokesReceivePackProcess{ 163 | Cmd: srp, 164 | In: srpIn, 165 | Out: bufSRPOut, 166 | Err: srpErr, 167 | } 168 | } 169 | 170 | type refUpdate struct { 171 | OldOID, NewOID, Ref string 172 | } 173 | 174 | func writePushData(t *testing.T, srp spokesReceivePackProcess, updates []refUpdate, pack io.Reader) { 175 | caps := "\x00report-status report-status-v2 side-band-64k object-format=sha1\n" 176 | for _, up := range updates { 177 | require.NoError(t, writePktlinef(srp.In, 178 | "%s %s %s%s", 179 | up.OldOID, up.NewOID, up.Ref, 180 | caps)) 181 | caps = "" 182 | } 183 | 184 | _, err := srp.In.Write([]byte("0000")) 185 | require.NoError(t, err) 186 | 187 | if _, err := io.Copy(srp.In, pack); err != nil { 188 | t.Logf("error writing pack to spokes-receive-pack input: %v", err) 189 | } 190 | 191 | require.NoError(t, srp.In.Close()) 192 | } 193 | -------------------------------------------------------------------------------- /internal/integration/nosideband_test.go: -------------------------------------------------------------------------------- 1 | //go:build integration 2 | 3 | package integration 4 | 5 | import ( 6 | "bufio" 7 | "bytes" 8 | "context" 9 | "fmt" 10 | "io" 11 | "os" 12 | "os/exec" 13 | "path/filepath" 14 | "testing" 15 | "time" 16 | 17 | "github.com/stretchr/testify/assert" 18 | "github.com/stretchr/testify/require" 19 | ) 20 | 21 | func TestNoSideBand(t *testing.T) { 22 | const ( 23 | defaultBranch = "refs/heads/main" 24 | createBranch = "refs/heads/newbranch" 25 | 26 | testCommit = "e589bdee50e39beac56220c4b7a716225f79e3cf" 27 | ) 28 | 29 | wd, err := os.Getwd() 30 | require.NoError(t, err) 31 | origin := filepath.Join(wd, "testdata/remote/git-internals-fork.git") 32 | 33 | testRepo := t.TempDir() 34 | requireRun(t, "git", "init", "--bare", testRepo) 35 | requireRun(t, "git", "-C", testRepo, "fetch", origin, defaultBranch+":"+defaultBranch) 36 | 37 | ctx, cancel := context.WithTimeout(context.Background(), time.Second) 38 | defer cancel() 39 | 40 | srp := exec.CommandContext(ctx, "spokes-receive-pack", ".") 41 | srp.Dir = testRepo 42 | srp.Env = append(os.Environ(), 43 | "GIT_SOCKSTAT_VAR_spokes_quarantine=bool:true", 44 | "GIT_SOCKSTAT_VAR_quarantine_id=config-test-quarantine-id") 45 | srp.Stderr = &testLogWriter{t} 46 | srpIn, err := srp.StdinPipe() 47 | require.NoError(t, err) 48 | srpOut, err := srp.StdoutPipe() 49 | require.NoError(t, err) 50 | 51 | srpErr := make(chan error) 52 | go func() { srpErr <- srp.Run() }() 53 | 54 | bufSRPOut := bufio.NewReader(srpOut) 55 | 56 | refs, _, err := readAdv(bufSRPOut) 57 | require.NoError(t, err) 58 | assert.Equal(t, refs, map[string]string{ 59 | defaultBranch: testCommit, 60 | }) 61 | 62 | oldnew := fmt.Sprintf("%040d %s", 0, testCommit) 63 | require.NoError(t, writePktlinef(srpIn, 64 | "%s %s\x00report-status report-status-v2 push-options object-format=sha1\n", oldnew, createBranch)) 65 | _, err = srpIn.Write([]byte("0000")) 66 | require.NoError(t, err) 67 | 68 | require.NoError(t, writePktlinef(srpIn, 69 | "anything i want to put in a push option\n")) 70 | _, err = srpIn.Write([]byte("0000")) 71 | require.NoError(t, err) 72 | 73 | // Send an empty pack, since we're using commits that are already in 74 | // the repo. 75 | pack, err := os.Open("testdata/empty.pack") 76 | require.NoError(t, err) 77 | if _, err := io.Copy(srpIn, pack); err != nil { 78 | t.Logf("error writing pack to spokes-receive-pack input: %v", err) 79 | } 80 | 81 | require.NoError(t, srpIn.Close()) 82 | 83 | lines, err := readResultNoSideBand(t, bufSRPOut) 84 | require.NoError(t, err) 85 | assert.Equal(t, []string{ 86 | "unpack ok\n", 87 | "ok refs/heads/newbranch\n", 88 | }, lines) 89 | } 90 | 91 | func readResultNoSideBand(t *testing.T, r io.Reader) ([]string, error) { 92 | var lines []string 93 | 94 | // Read all of the output so that we can include it with errors. 95 | data, err := io.ReadAll(r) 96 | if err != nil { 97 | if len(data) > 0 { 98 | t.Logf("got data, but there was an error: %v", err) 99 | } else { 100 | return nil, err 101 | } 102 | } 103 | 104 | // Replace r. 105 | r = bytes.NewReader(data) 106 | 107 | for { 108 | pkt, err := readPktline(r) 109 | switch { 110 | case err != nil: 111 | return nil, fmt.Errorf("%w while parsing %q", err, string(data)) 112 | 113 | case pkt == nil: 114 | return lines, nil 115 | 116 | default: 117 | lines = append(lines, string(pkt)) 118 | } 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /internal/integration/parse.go: -------------------------------------------------------------------------------- 1 | //go:build integration 2 | 3 | package integration 4 | 5 | import ( 6 | "bytes" 7 | "fmt" 8 | "io" 9 | "os/exec" 10 | "strconv" 11 | "strings" 12 | "testing" 13 | 14 | "github.com/stretchr/testify/require" 15 | ) 16 | 17 | func readAdv(r io.Reader) (map[string]string, string, error) { 18 | caps := "" 19 | refs := make(map[string]string) 20 | firstLine := true 21 | for { 22 | data, err := readPktline(r) 23 | if err != nil { 24 | return nil, "", err 25 | } 26 | if data == nil { 27 | return refs, caps, nil 28 | } 29 | 30 | if firstLine { 31 | parts := bytes.SplitN(data, []byte{0}, 2) 32 | if len(parts) != 2 { 33 | return nil, "", fmt.Errorf("expected capabilities on first line of ref advertisement %q", string(data)) 34 | } 35 | data = parts[0] 36 | caps = string(parts[1]) 37 | firstLine = false 38 | } 39 | 40 | parts := bytes.SplitN(data, []byte(" "), 2) 41 | if len(parts) != 2 || len(parts[0]) != 40 { 42 | return nil, "", fmt.Errorf("bad advertisement line: %q", string(data)) 43 | } 44 | oid := string(parts[0]) 45 | refName := strings.TrimSuffix(string(parts[1]), "\n") 46 | if _, ok := refs[refName]; ok { 47 | return nil, "", fmt.Errorf("duplicate entry for %q", refName) 48 | } 49 | refs[refName] = oid 50 | } 51 | } 52 | 53 | func readResult(t *testing.T, r io.Reader) (map[string]string, string, [][]byte, error) { 54 | var ( 55 | refStatus map[string]string 56 | unpackRes string 57 | sideband [][]byte 58 | ) 59 | 60 | // Read all of the output so that we can include it with errors. 61 | data, err := io.ReadAll(r) 62 | if err != nil { 63 | if len(data) > 0 { 64 | t.Logf("got data, but there was an error: %v", err) 65 | } else { 66 | return nil, "", nil, err 67 | } 68 | } 69 | 70 | // Replace r. 71 | r = bytes.NewReader(data) 72 | 73 | for { 74 | pkt, err := readPktline(r) 75 | switch { 76 | case err != nil: 77 | return nil, "", nil, fmt.Errorf("%w while parsing %q", err, string(data)) 78 | 79 | case pkt == nil: 80 | if refStatus == nil { 81 | return nil, "", nil, fmt.Errorf("no sideband 1 packet in %q", string(data)) 82 | } 83 | return refStatus, unpackRes, sideband, nil 84 | 85 | case bytes.HasPrefix(pkt, []byte{1}): 86 | if refStatus != nil { 87 | return nil, "", nil, fmt.Errorf("repeated sideband 1 packet in %q", string(data)) 88 | } 89 | refStatus, unpackRes, err = parseSideband1(pkt[1:]) 90 | if err != nil { 91 | return nil, "", nil, err 92 | } 93 | 94 | case bytes.HasPrefix(pkt, []byte{2}): 95 | sideband = append(sideband, append([]byte{}, data...)) 96 | 97 | default: 98 | return nil, "", nil, fmt.Errorf("todo: handle %q from %q", string(pkt), string(data)) 99 | } 100 | } 101 | } 102 | 103 | func parseSideband1(data []byte) (map[string]string, string, error) { 104 | refs := make(map[string]string) 105 | unpack := "" 106 | 107 | r := bytes.NewReader(data) 108 | 109 | for { 110 | pkt, err := readPktline(r) 111 | switch { 112 | case err != nil: 113 | return nil, "", fmt.Errorf("%w while parsing sideband 1 packet %q", err, string(data)) 114 | 115 | case pkt == nil: 116 | return refs, unpack, nil 117 | 118 | case bytes.HasPrefix(pkt, []byte("unpack ")): 119 | unpack = unpack + string(pkt) 120 | 121 | case bytes.HasPrefix(pkt, []byte("ng ")): 122 | parts := bytes.SplitN(bytes.TrimSuffix(pkt[3:], []byte("\n")), []byte(" "), 2) 123 | if len(parts) == 2 { 124 | refs[string(parts[0])] = "ng " + string(parts[1]) 125 | } else { 126 | refs[string(parts[0])] = "ng" 127 | } 128 | 129 | case len(pkt) > 3 && pkt[2] == ' ': 130 | refs[string(bytes.TrimSuffix(pkt[3:], []byte("\n")))] = string(pkt[0:2]) 131 | 132 | default: 133 | return nil, "", fmt.Errorf("unrecognized status %q in sideband 1 packet %q", string(pkt), string(data)) 134 | } 135 | } 136 | } 137 | 138 | func readPktline(r io.Reader) ([]byte, error) { 139 | sizeBuf := make([]byte, 4) 140 | n, err := r.Read(sizeBuf) 141 | if err != nil { 142 | return nil, err 143 | } 144 | if n != 4 { 145 | return nil, fmt.Errorf("expected 4 bytes but got %d (%s)", n, sizeBuf[:n]) 146 | } 147 | 148 | size, err := strconv.ParseUint(string(sizeBuf), 16, 16) 149 | if err != nil { 150 | return nil, err 151 | } 152 | 153 | if size == 0 { 154 | return nil, nil 155 | } 156 | if size < 4 { 157 | return nil, fmt.Errorf("invalid length %q", sizeBuf) 158 | } 159 | 160 | buf := make([]byte, size-4) 161 | n, err = io.ReadFull(r, buf) 162 | return buf, err 163 | } 164 | 165 | func writePktlinef(w io.Writer, format string, args ...interface{}) error { 166 | msg := fmt.Sprintf(format, args...) 167 | err := writePktline(w, msg) 168 | return err 169 | } 170 | 171 | func writePktline(w io.Writer, msg string) error { 172 | _, err := fmt.Fprintf(w, "%04x%s", 4+len(msg), msg) 173 | return err 174 | } 175 | 176 | func requireRun(t *testing.T, program string, args ...string) { 177 | t.Logf("run %s %v", program, args) 178 | cmd := exec.Command(program, args...) 179 | out, err := cmd.CombinedOutput() 180 | if len(out) > 0 { 181 | t.Logf("%s", out) 182 | } 183 | require.NoError(t, err, "%s %v:\n%s", program, args, out) 184 | } 185 | -------------------------------------------------------------------------------- /internal/integration/pushoptions_test.go: -------------------------------------------------------------------------------- 1 | //go:build integration 2 | 3 | package integration 4 | 5 | import ( 6 | "bufio" 7 | "context" 8 | "fmt" 9 | "io" 10 | "os" 11 | "os/exec" 12 | "path/filepath" 13 | "testing" 14 | "time" 15 | 16 | "github.com/stretchr/testify/assert" 17 | "github.com/stretchr/testify/require" 18 | ) 19 | 20 | const ( 21 | defaultBranch = "refs/heads/main" 22 | createBranch = "refs/heads/newbranch" 23 | 24 | testCommit = "e589bdee50e39beac56220c4b7a716225f79e3cf" 25 | ) 26 | 27 | func setupTestRepo(t *testing.T) string { 28 | wd, err := os.Getwd() 29 | require.NoError(t, err) 30 | origin := filepath.Join(wd, "testdata/remote/git-internals-fork.git") 31 | 32 | testRepo := t.TempDir() 33 | requireRun(t, "git", "init", "--bare", testRepo) 34 | requireRun(t, "git", "-C", testRepo, "fetch", origin, defaultBranch+":"+defaultBranch) 35 | 36 | return testRepo 37 | } 38 | 39 | func TestPushOptions(t *testing.T) { 40 | testRepo := setupTestRepo(t) 41 | 42 | ctx, cancel := context.WithTimeout(context.Background(), time.Second) 43 | defer cancel() 44 | 45 | srp := exec.CommandContext(ctx, "spokes-receive-pack", ".") 46 | srp.Dir = testRepo 47 | srp.Env = append(os.Environ(), 48 | "GIT_SOCKSTAT_VAR_spokes_quarantine=bool:true", 49 | "GIT_SOCKSTAT_VAR_quarantine_id=config-test-quarantine-id") 50 | srp.Stderr = &testLogWriter{t} 51 | srpIn, err := srp.StdinPipe() 52 | require.NoError(t, err) 53 | srpOut, err := srp.StdoutPipe() 54 | require.NoError(t, err) 55 | 56 | srpErr := make(chan error) 57 | go func() { srpErr <- srp.Run() }() 58 | 59 | bufSRPOut := bufio.NewReader(srpOut) 60 | 61 | refs, _, err := readAdv(bufSRPOut) 62 | require.NoError(t, err) 63 | assert.Equal(t, refs, map[string]string{ 64 | defaultBranch: testCommit, 65 | }) 66 | 67 | oldnew := fmt.Sprintf("%040d %s", 0, testCommit) 68 | require.NoError(t, writePktlinef(srpIn, 69 | "%s %s\x00report-status report-status-v2 side-band-64k push-options object-format=sha1\n", oldnew, createBranch)) 70 | _, err = srpIn.Write([]byte("0000")) 71 | require.NoError(t, err) 72 | 73 | require.NoError(t, writePktlinef(srpIn, 74 | "anything i want to put in a push option\n")) 75 | _, err = srpIn.Write([]byte("0000")) 76 | require.NoError(t, err) 77 | 78 | // Send an empty pack, since we're using commits that are already in 79 | // the repo. 80 | pack, err := os.Open("testdata/empty.pack") 81 | require.NoError(t, err) 82 | if _, err := io.Copy(srpIn, pack); err != nil { 83 | t.Logf("error writing pack to spokes-receive-pack input: %v", err) 84 | } 85 | 86 | require.NoError(t, srpIn.Close()) 87 | 88 | refStatus, unpackRes, _, err := readResult(t, bufSRPOut) 89 | require.NoError(t, err) 90 | assert.Equal(t, map[string]string{ 91 | createBranch: "ok", 92 | }, refStatus) 93 | assert.Equal(t, "unpack ok\n", unpackRes) 94 | } 95 | 96 | func TestPushOptionsLimitCount(t *testing.T) { 97 | testRepo := setupTestRepo(t) 98 | requireRun(t, "git", "-C", testRepo, "config", "receive.pushOptionsCountLimit", "2") 99 | 100 | ctx, cancel := context.WithTimeout(context.Background(), time.Second) 101 | defer cancel() 102 | 103 | srp := exec.CommandContext(ctx, "spokes-receive-pack", ".") 104 | srp.Dir = testRepo 105 | srp.Env = append(os.Environ(), 106 | "GIT_SOCKSTAT_VAR_spokes_quarantine=bool:true", 107 | "GIT_SOCKSTAT_VAR_quarantine_id=config-test-quarantine-id") 108 | srp.Stderr = &testLogWriter{t} 109 | srpIn, err := srp.StdinPipe() 110 | require.NoError(t, err) 111 | srpOut, err := srp.StdoutPipe() 112 | require.NoError(t, err) 113 | 114 | srpErr := make(chan error) 115 | go func() { srpErr <- srp.Run() }() 116 | 117 | bufSRPOut := bufio.NewReader(srpOut) 118 | 119 | refs, _, err := readAdv(bufSRPOut) 120 | require.NoError(t, err) 121 | assert.Equal(t, refs, map[string]string{ 122 | defaultBranch: testCommit, 123 | }) 124 | 125 | oldnew := fmt.Sprintf("%040d %s", 0, testCommit) 126 | require.NoError(t, writePktlinef(srpIn, 127 | "%s %s\x00report-status report-status-v2 side-band-64k push-options object-format=sha1\n", oldnew, createBranch)) 128 | _, err = srpIn.Write([]byte("0000")) 129 | require.NoError(t, err) 130 | 131 | // the limit is 2, let's send 3 push options 132 | for i := 0; i < 3; i++ { 133 | option := fmt.Sprintf("option-%d\n", i) 134 | require.NoError(t, writePktline(srpIn, option)) 135 | } 136 | _, err = srpIn.Write([]byte("0000")) 137 | require.NoError(t, err) 138 | 139 | // Send an example pack, since we're using commits that are already in 140 | // the repo. 141 | pack, err := os.Open("testdata/empty.pack") 142 | require.NoError(t, err) 143 | if _, err := io.Copy(srpIn, pack); err != nil { 144 | t.Logf("error writing pack to spokes-receive-pack input: %v", err) 145 | } 146 | 147 | require.NoError(t, srpIn.Close()) 148 | 149 | refStatus, unpackRes, _, err := readResult(t, bufSRPOut) 150 | require.NoError(t, err) 151 | assert.Equal(t, map[string]string{ 152 | createBranch: "ng push options count exceeds maximum", 153 | }, refStatus) 154 | assert.Equal(t, "unpack ok\n", unpackRes) 155 | } 156 | -------------------------------------------------------------------------------- /internal/integration/testdata/bad-date/sha1.git/HEAD: -------------------------------------------------------------------------------- 1 | ref: refs/heads/main 2 | -------------------------------------------------------------------------------- /internal/integration/testdata/bad-date/sha1.git/config: -------------------------------------------------------------------------------- 1 | [core] 2 | repositoryformatversion = 0 3 | filemode = true 4 | bare = true 5 | -------------------------------------------------------------------------------- /internal/integration/testdata/bad-date/sha1.git/description: -------------------------------------------------------------------------------- 1 | Unnamed repository; edit this file 'description' to name the repository. 2 | -------------------------------------------------------------------------------- /internal/integration/testdata/bad-date/sha1.git/info/refs: -------------------------------------------------------------------------------- 1 | e1971a634e8b1e52b09eba0d21d03ec291c6b690 refs/heads/main 2 | -------------------------------------------------------------------------------- /internal/integration/testdata/bad-date/sha1.git/objects/info/packs: -------------------------------------------------------------------------------- 1 | P pack-01f7c523f4ab85d86dbe8b116ab6cd69e87497f3.pack 2 | 3 | -------------------------------------------------------------------------------- /internal/integration/testdata/bad-date/sha1.git/objects/pack/pack-01f7c523f4ab85d86dbe8b116ab6cd69e87497f3.bitmap: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/spokes-receive-pack/acac8763c60f636c44baaf5c3887895cf5f55c30/internal/integration/testdata/bad-date/sha1.git/objects/pack/pack-01f7c523f4ab85d86dbe8b116ab6cd69e87497f3.bitmap -------------------------------------------------------------------------------- /internal/integration/testdata/bad-date/sha1.git/objects/pack/pack-01f7c523f4ab85d86dbe8b116ab6cd69e87497f3.idx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/spokes-receive-pack/acac8763c60f636c44baaf5c3887895cf5f55c30/internal/integration/testdata/bad-date/sha1.git/objects/pack/pack-01f7c523f4ab85d86dbe8b116ab6cd69e87497f3.idx -------------------------------------------------------------------------------- /internal/integration/testdata/bad-date/sha1.git/objects/pack/pack-01f7c523f4ab85d86dbe8b116ab6cd69e87497f3.pack: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/spokes-receive-pack/acac8763c60f636c44baaf5c3887895cf5f55c30/internal/integration/testdata/bad-date/sha1.git/objects/pack/pack-01f7c523f4ab85d86dbe8b116ab6cd69e87497f3.pack -------------------------------------------------------------------------------- /internal/integration/testdata/bad-date/sha1.git/packed-refs: -------------------------------------------------------------------------------- 1 | # pack-refs with: peeled fully-peeled sorted 2 | e1971a634e8b1e52b09eba0d21d03ec291c6b690 refs/heads/main 3 | -------------------------------------------------------------------------------- /internal/integration/testdata/bad-date/sha1.git/refs/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/spokes-receive-pack/acac8763c60f636c44baaf5c3887895cf5f55c30/internal/integration/testdata/bad-date/sha1.git/refs/.keep -------------------------------------------------------------------------------- /internal/integration/testdata/bad-date/sha256.git/HEAD: -------------------------------------------------------------------------------- 1 | ref: refs/heads/main 2 | -------------------------------------------------------------------------------- /internal/integration/testdata/bad-date/sha256.git/config: -------------------------------------------------------------------------------- 1 | [core] 2 | repositoryformatversion = 1 3 | filemode = true 4 | bare = true 5 | [extensions] 6 | objectformat = sha256 7 | -------------------------------------------------------------------------------- /internal/integration/testdata/bad-date/sha256.git/description: -------------------------------------------------------------------------------- 1 | Unnamed repository; edit this file 'description' to name the repository. 2 | -------------------------------------------------------------------------------- /internal/integration/testdata/bad-date/sha256.git/info/refs: -------------------------------------------------------------------------------- 1 | 8ae390f74dd659799c5e9f9bff315143347f30961df0a61f2b23132521f25a47 refs/heads/main 2 | -------------------------------------------------------------------------------- /internal/integration/testdata/bad-date/sha256.git/objects/info/packs: -------------------------------------------------------------------------------- 1 | P pack-1a1bc6a0c21b6c3b7683df993586a57ec40580603ee77348431ab8d4f72b55f0.pack 2 | 3 | -------------------------------------------------------------------------------- /internal/integration/testdata/bad-date/sha256.git/objects/pack/pack-1a1bc6a0c21b6c3b7683df993586a57ec40580603ee77348431ab8d4f72b55f0.bitmap: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/spokes-receive-pack/acac8763c60f636c44baaf5c3887895cf5f55c30/internal/integration/testdata/bad-date/sha256.git/objects/pack/pack-1a1bc6a0c21b6c3b7683df993586a57ec40580603ee77348431ab8d4f72b55f0.bitmap -------------------------------------------------------------------------------- /internal/integration/testdata/bad-date/sha256.git/objects/pack/pack-1a1bc6a0c21b6c3b7683df993586a57ec40580603ee77348431ab8d4f72b55f0.idx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/spokes-receive-pack/acac8763c60f636c44baaf5c3887895cf5f55c30/internal/integration/testdata/bad-date/sha256.git/objects/pack/pack-1a1bc6a0c21b6c3b7683df993586a57ec40580603ee77348431ab8d4f72b55f0.idx -------------------------------------------------------------------------------- /internal/integration/testdata/bad-date/sha256.git/objects/pack/pack-1a1bc6a0c21b6c3b7683df993586a57ec40580603ee77348431ab8d4f72b55f0.pack: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/spokes-receive-pack/acac8763c60f636c44baaf5c3887895cf5f55c30/internal/integration/testdata/bad-date/sha256.git/objects/pack/pack-1a1bc6a0c21b6c3b7683df993586a57ec40580603ee77348431ab8d4f72b55f0.pack -------------------------------------------------------------------------------- /internal/integration/testdata/bad-date/sha256.git/packed-refs: -------------------------------------------------------------------------------- 1 | # pack-refs with: peeled fully-peeled sorted 2 | 8ae390f74dd659799c5e9f9bff315143347f30961df0a61f2b23132521f25a47 refs/heads/main 3 | -------------------------------------------------------------------------------- /internal/integration/testdata/bad-date/sha256.git/refs/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/spokes-receive-pack/acac8763c60f636c44baaf5c3887895cf5f55c30/internal/integration/testdata/bad-date/sha256.git/refs/.keep -------------------------------------------------------------------------------- /internal/integration/testdata/empty.pack: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/spokes-receive-pack/acac8763c60f636c44baaf5c3887895cf5f55c30/internal/integration/testdata/empty.pack -------------------------------------------------------------------------------- /internal/integration/testdata/gitconfig: -------------------------------------------------------------------------------- 1 | # This file has a subset of options from our prod /etc/gitconfig. 2 | 3 | [receive] 4 | fsckObjects = true 5 | -------------------------------------------------------------------------------- /internal/integration/testdata/missing-objects/bad.pack: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/spokes-receive-pack/acac8763c60f636c44baaf5c3887895cf5f55c30/internal/integration/testdata/missing-objects/bad.pack -------------------------------------------------------------------------------- /internal/integration/testdata/missing-objects/empty.pack: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/spokes-receive-pack/acac8763c60f636c44baaf5c3887895cf5f55c30/internal/integration/testdata/missing-objects/empty.pack -------------------------------------------------------------------------------- /internal/integration/testdata/missing-objects/info.json: -------------------------------------------------------------------------------- 1 | { 2 | "push_from": "6389baef05c2b5e5c9f603daa23df53e71bdf591", 3 | "push_to": "04ae1b030995df0ae3e91c41d1a065aaae405397", 4 | "ref": "refs/heads/example", 5 | "extra_ref": "refs/heads/other" 6 | } 7 | -------------------------------------------------------------------------------- /internal/integration/testdata/missing-objects/remote.git/HEAD: -------------------------------------------------------------------------------- 1 | ref: refs/heads/example 2 | -------------------------------------------------------------------------------- /internal/integration/testdata/missing-objects/remote.git/config: -------------------------------------------------------------------------------- 1 | [core] 2 | repositoryformatversion = 0 3 | filemode = true 4 | bare = true 5 | ignorecase = true 6 | precomposeunicode = true 7 | -------------------------------------------------------------------------------- /internal/integration/testdata/missing-objects/remote.git/description: -------------------------------------------------------------------------------- 1 | Unnamed repository; edit this file 'description' to name the repository. 2 | -------------------------------------------------------------------------------- /internal/integration/testdata/missing-objects/remote.git/info/refs: -------------------------------------------------------------------------------- 1 | 6389baef05c2b5e5c9f603daa23df53e71bdf591 refs/heads/example 2 | 6389baef05c2b5e5c9f603daa23df53e71bdf591 refs/heads/other 3 | -------------------------------------------------------------------------------- /internal/integration/testdata/missing-objects/remote.git/objects/info/packs: -------------------------------------------------------------------------------- 1 | P pack-3e83f112afcee29136d3e21f295a9ba4d76ce184.pack 2 | 3 | -------------------------------------------------------------------------------- /internal/integration/testdata/missing-objects/remote.git/objects/pack/pack-3e83f112afcee29136d3e21f295a9ba4d76ce184.bitmap: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/spokes-receive-pack/acac8763c60f636c44baaf5c3887895cf5f55c30/internal/integration/testdata/missing-objects/remote.git/objects/pack/pack-3e83f112afcee29136d3e21f295a9ba4d76ce184.bitmap -------------------------------------------------------------------------------- /internal/integration/testdata/missing-objects/remote.git/objects/pack/pack-3e83f112afcee29136d3e21f295a9ba4d76ce184.idx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/spokes-receive-pack/acac8763c60f636c44baaf5c3887895cf5f55c30/internal/integration/testdata/missing-objects/remote.git/objects/pack/pack-3e83f112afcee29136d3e21f295a9ba4d76ce184.idx -------------------------------------------------------------------------------- /internal/integration/testdata/missing-objects/remote.git/objects/pack/pack-3e83f112afcee29136d3e21f295a9ba4d76ce184.pack: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/spokes-receive-pack/acac8763c60f636c44baaf5c3887895cf5f55c30/internal/integration/testdata/missing-objects/remote.git/objects/pack/pack-3e83f112afcee29136d3e21f295a9ba4d76ce184.pack -------------------------------------------------------------------------------- /internal/integration/testdata/missing-objects/remote.git/packed-refs: -------------------------------------------------------------------------------- 1 | # pack-refs with: peeled fully-peeled sorted 2 | 6389baef05c2b5e5c9f603daa23df53e71bdf591 refs/heads/example 3 | 6389baef05c2b5e5c9f603daa23df53e71bdf591 refs/heads/other 4 | -------------------------------------------------------------------------------- /internal/integration/testdata/missing-objects/remote.git/refs/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/spokes-receive-pack/acac8763c60f636c44baaf5c3887895cf5f55c30/internal/integration/testdata/missing-objects/remote.git/refs/.keep -------------------------------------------------------------------------------- /internal/integration/testdata/remote/git-internals-fork.git/HEAD: -------------------------------------------------------------------------------- 1 | ref: refs/heads/main 2 | -------------------------------------------------------------------------------- /internal/integration/testdata/remote/git-internals-fork.git/config: -------------------------------------------------------------------------------- 1 | [core] 2 | repositoryformatversion = 0 3 | filemode = true 4 | bare = true 5 | ignorecase = true 6 | precomposeunicode = true 7 | -------------------------------------------------------------------------------- /internal/integration/testdata/remote/git-internals-fork.git/description: -------------------------------------------------------------------------------- 1 | Unnamed repository; edit this file 'description' to name the repository. 2 | -------------------------------------------------------------------------------- /internal/integration/testdata/remote/git-internals-fork.git/hooks/applypatch-msg.sample: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # An example hook script to check the commit log message taken by 4 | # applypatch from an e-mail message. 5 | # 6 | # The hook should exit with non-zero status after issuing an 7 | # appropriate message if it wants to stop the commit. The hook is 8 | # allowed to edit the commit message file. 9 | # 10 | # To enable this hook, rename this file to "applypatch-msg". 11 | 12 | . git-sh-setup 13 | commitmsg="$(git rev-parse --git-path hooks/commit-msg)" 14 | test -x "$commitmsg" && exec "$commitmsg" ${1+"$@"} 15 | : 16 | -------------------------------------------------------------------------------- /internal/integration/testdata/remote/git-internals-fork.git/hooks/commit-msg.sample: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # An example hook script to check the commit log message. 4 | # Called by "git commit" with one argument, the name of the file 5 | # that has the commit message. The hook should exit with non-zero 6 | # status after issuing an appropriate message if it wants to stop the 7 | # commit. The hook is allowed to edit the commit message file. 8 | # 9 | # To enable this hook, rename this file to "commit-msg". 10 | 11 | # Uncomment the below to add a Signed-off-by line to the message. 12 | # Doing this in a hook is a bad idea in general, but the prepare-commit-msg 13 | # hook is more suited to it. 14 | # 15 | # SOB=$(git var GIT_AUTHOR_IDENT | sed -n 's/^\(.*>\).*$/Signed-off-by: \1/p') 16 | # grep -qs "^$SOB" "$1" || echo "$SOB" >> "$1" 17 | 18 | # This example catches duplicate Signed-off-by lines. 19 | 20 | test "" = "$(grep '^Signed-off-by: ' "$1" | 21 | sort | uniq -c | sed -e '/^[ ]*1[ ]/d')" || { 22 | echo >&2 Duplicate Signed-off-by lines. 23 | exit 1 24 | } 25 | -------------------------------------------------------------------------------- /internal/integration/testdata/remote/git-internals-fork.git/hooks/fsmonitor-watchman.sample: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl 2 | 3 | use strict; 4 | use warnings; 5 | use IPC::Open2; 6 | 7 | # An example hook script to integrate Watchman 8 | # (https://facebook.github.io/watchman/) with git to speed up detecting 9 | # new and modified files. 10 | # 11 | # The hook is passed a version (currently 2) and last update token 12 | # formatted as a string and outputs to stdout a new update token and 13 | # all files that have been modified since the update token. Paths must 14 | # be relative to the root of the working tree and separated by a single NUL. 15 | # 16 | # To enable this hook, rename this file to "query-watchman" and set 17 | # 'git config core.fsmonitor .git/hooks/query-watchman' 18 | # 19 | my ($version, $last_update_token) = @ARGV; 20 | 21 | # Uncomment for debugging 22 | # print STDERR "$0 $version $last_update_token\n"; 23 | 24 | # Check the hook interface version 25 | if ($version ne 2) { 26 | die "Unsupported query-fsmonitor hook version '$version'.\n" . 27 | "Falling back to scanning...\n"; 28 | } 29 | 30 | my $git_work_tree = get_working_dir(); 31 | 32 | my $retry = 1; 33 | 34 | my $json_pkg; 35 | eval { 36 | require JSON::XS; 37 | $json_pkg = "JSON::XS"; 38 | 1; 39 | } or do { 40 | require JSON::PP; 41 | $json_pkg = "JSON::PP"; 42 | }; 43 | 44 | launch_watchman(); 45 | 46 | sub launch_watchman { 47 | my $o = watchman_query(); 48 | if (is_work_tree_watched($o)) { 49 | output_result($o->{clock}, @{$o->{files}}); 50 | } 51 | } 52 | 53 | sub output_result { 54 | my ($clockid, @files) = @_; 55 | 56 | # Uncomment for debugging watchman output 57 | # open (my $fh, ">", ".git/watchman-output.out"); 58 | # binmode $fh, ":utf8"; 59 | # print $fh "$clockid\n@files\n"; 60 | # close $fh; 61 | 62 | binmode STDOUT, ":utf8"; 63 | print $clockid; 64 | print "\0"; 65 | local $, = "\0"; 66 | print @files; 67 | } 68 | 69 | sub watchman_clock { 70 | my $response = qx/watchman clock "$git_work_tree"/; 71 | die "Failed to get clock id on '$git_work_tree'.\n" . 72 | "Falling back to scanning...\n" if $? != 0; 73 | 74 | return $json_pkg->new->utf8->decode($response); 75 | } 76 | 77 | sub watchman_query { 78 | my $pid = open2(\*CHLD_OUT, \*CHLD_IN, 'watchman -j --no-pretty') 79 | or die "open2() failed: $!\n" . 80 | "Falling back to scanning...\n"; 81 | 82 | # In the query expression below we're asking for names of files that 83 | # changed since $last_update_token but not from the .git folder. 84 | # 85 | # To accomplish this, we're using the "since" generator to use the 86 | # recency index to select candidate nodes and "fields" to limit the 87 | # output to file names only. Then we're using the "expression" term to 88 | # further constrain the results. 89 | my $last_update_line = ""; 90 | if (substr($last_update_token, 0, 1) eq "c") { 91 | $last_update_token = "\"$last_update_token\""; 92 | $last_update_line = qq[\n"since": $last_update_token,]; 93 | } 94 | my $query = <<" END"; 95 | ["query", "$git_work_tree", {$last_update_line 96 | "fields": ["name"], 97 | "expression": ["not", ["dirname", ".git"]] 98 | }] 99 | END 100 | 101 | # Uncomment for debugging the watchman query 102 | # open (my $fh, ">", ".git/watchman-query.json"); 103 | # print $fh $query; 104 | # close $fh; 105 | 106 | print CHLD_IN $query; 107 | close CHLD_IN; 108 | my $response = do {local $/; }; 109 | 110 | # Uncomment for debugging the watch response 111 | # open ($fh, ">", ".git/watchman-response.json"); 112 | # print $fh $response; 113 | # close $fh; 114 | 115 | die "Watchman: command returned no output.\n" . 116 | "Falling back to scanning...\n" if $response eq ""; 117 | die "Watchman: command returned invalid output: $response\n" . 118 | "Falling back to scanning...\n" unless $response =~ /^\{/; 119 | 120 | return $json_pkg->new->utf8->decode($response); 121 | } 122 | 123 | sub is_work_tree_watched { 124 | my ($output) = @_; 125 | my $error = $output->{error}; 126 | if ($retry > 0 and $error and $error =~ m/unable to resolve root .* directory (.*) is not watched/) { 127 | $retry--; 128 | my $response = qx/watchman watch "$git_work_tree"/; 129 | die "Failed to make watchman watch '$git_work_tree'.\n" . 130 | "Falling back to scanning...\n" if $? != 0; 131 | $output = $json_pkg->new->utf8->decode($response); 132 | $error = $output->{error}; 133 | die "Watchman: $error.\n" . 134 | "Falling back to scanning...\n" if $error; 135 | 136 | # Uncomment for debugging watchman output 137 | # open (my $fh, ">", ".git/watchman-output.out"); 138 | # close $fh; 139 | 140 | # Watchman will always return all files on the first query so 141 | # return the fast "everything is dirty" flag to git and do the 142 | # Watchman query just to get it over with now so we won't pay 143 | # the cost in git to look up each individual file. 144 | my $o = watchman_clock(); 145 | $error = $output->{error}; 146 | 147 | die "Watchman: $error.\n" . 148 | "Falling back to scanning...\n" if $error; 149 | 150 | output_result($o->{clock}, ("/")); 151 | $last_update_token = $o->{clock}; 152 | 153 | eval { launch_watchman() }; 154 | return 0; 155 | } 156 | 157 | die "Watchman: $error.\n" . 158 | "Falling back to scanning...\n" if $error; 159 | 160 | return 1; 161 | } 162 | 163 | sub get_working_dir { 164 | my $working_dir; 165 | if ($^O =~ 'msys' || $^O =~ 'cygwin') { 166 | $working_dir = Win32::GetCwd(); 167 | $working_dir =~ tr/\\/\//; 168 | } else { 169 | require Cwd; 170 | $working_dir = Cwd::cwd(); 171 | } 172 | 173 | return $working_dir; 174 | } 175 | -------------------------------------------------------------------------------- /internal/integration/testdata/remote/git-internals-fork.git/hooks/post-update.sample: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # An example hook script to prepare a packed repository for use over 4 | # dumb transports. 5 | # 6 | # To enable this hook, rename this file to "post-update". 7 | 8 | exec git update-server-info 9 | -------------------------------------------------------------------------------- /internal/integration/testdata/remote/git-internals-fork.git/hooks/pre-applypatch.sample: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # An example hook script to verify what is about to be committed 4 | # by applypatch from an e-mail message. 5 | # 6 | # The hook should exit with non-zero status after issuing an 7 | # appropriate message if it wants to stop the commit. 8 | # 9 | # To enable this hook, rename this file to "pre-applypatch". 10 | 11 | . git-sh-setup 12 | precommit="$(git rev-parse --git-path hooks/pre-commit)" 13 | test -x "$precommit" && exec "$precommit" ${1+"$@"} 14 | : 15 | -------------------------------------------------------------------------------- /internal/integration/testdata/remote/git-internals-fork.git/hooks/pre-commit.sample: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # An example hook script to verify what is about to be committed. 4 | # Called by "git commit" with no arguments. The hook should 5 | # exit with non-zero status after issuing an appropriate message if 6 | # it wants to stop the commit. 7 | # 8 | # To enable this hook, rename this file to "pre-commit". 9 | 10 | if git rev-parse --verify HEAD >/dev/null 2>&1 11 | then 12 | against=HEAD 13 | else 14 | # Initial commit: diff against an empty tree object 15 | against=$(git hash-object -t tree /dev/null) 16 | fi 17 | 18 | # If you want to allow non-ASCII filenames set this variable to true. 19 | allownonascii=$(git config --type=bool hooks.allownonascii) 20 | 21 | # Redirect output to stderr. 22 | exec 1>&2 23 | 24 | # Cross platform projects tend to avoid non-ASCII filenames; prevent 25 | # them from being added to the repository. We exploit the fact that the 26 | # printable range starts at the space character and ends with tilde. 27 | if [ "$allownonascii" != "true" ] && 28 | # Note that the use of brackets around a tr range is ok here, (it's 29 | # even required, for portability to Solaris 10's /usr/bin/tr), since 30 | # the square bracket bytes happen to fall in the designated range. 31 | test $(git diff --cached --name-only --diff-filter=A -z $against | 32 | LC_ALL=C tr -d '[ -~]\0' | wc -c) != 0 33 | then 34 | cat <<\EOF 35 | Error: Attempt to add a non-ASCII file name. 36 | 37 | This can cause problems if you want to work with people on other platforms. 38 | 39 | To be portable it is advisable to rename the file. 40 | 41 | If you know what you are doing you can disable this check using: 42 | 43 | git config hooks.allownonascii true 44 | EOF 45 | exit 1 46 | fi 47 | 48 | # If there are whitespace errors, print the offending file names and fail. 49 | exec git diff-index --check --cached $against -- 50 | -------------------------------------------------------------------------------- /internal/integration/testdata/remote/git-internals-fork.git/hooks/pre-merge-commit.sample: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # An example hook script to verify what is about to be committed. 4 | # Called by "git merge" with no arguments. The hook should 5 | # exit with non-zero status after issuing an appropriate message to 6 | # stderr if it wants to stop the merge commit. 7 | # 8 | # To enable this hook, rename this file to "pre-merge-commit". 9 | 10 | . git-sh-setup 11 | test -x "$GIT_DIR/hooks/pre-commit" && 12 | exec "$GIT_DIR/hooks/pre-commit" 13 | : 14 | -------------------------------------------------------------------------------- /internal/integration/testdata/remote/git-internals-fork.git/hooks/pre-push.sample: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # An example hook script to verify what is about to be pushed. Called by "git 4 | # push" after it has checked the remote status, but before anything has been 5 | # pushed. If this script exits with a non-zero status nothing will be pushed. 6 | # 7 | # This hook is called with the following parameters: 8 | # 9 | # $1 -- Name of the remote to which the push is being done 10 | # $2 -- URL to which the push is being done 11 | # 12 | # If pushing without using a named remote those arguments will be equal. 13 | # 14 | # Information about the commits which are being pushed is supplied as lines to 15 | # the standard input in the form: 16 | # 17 | # 18 | # 19 | # This sample shows how to prevent push of commits where the log message starts 20 | # with "WIP" (work in progress). 21 | 22 | remote="$1" 23 | url="$2" 24 | 25 | zero=$(git hash-object --stdin &2 "Found WIP commit in $local_ref, not pushing" 48 | exit 1 49 | fi 50 | fi 51 | done 52 | 53 | exit 0 54 | -------------------------------------------------------------------------------- /internal/integration/testdata/remote/git-internals-fork.git/hooks/pre-rebase.sample: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # Copyright (c) 2006, 2008 Junio C Hamano 4 | # 5 | # The "pre-rebase" hook is run just before "git rebase" starts doing 6 | # its job, and can prevent the command from running by exiting with 7 | # non-zero status. 8 | # 9 | # The hook is called with the following parameters: 10 | # 11 | # $1 -- the upstream the series was forked from. 12 | # $2 -- the branch being rebased (or empty when rebasing the current branch). 13 | # 14 | # This sample shows how to prevent topic branches that are already 15 | # merged to 'next' branch from getting rebased, because allowing it 16 | # would result in rebasing already published history. 17 | 18 | publish=next 19 | basebranch="$1" 20 | if test "$#" = 2 21 | then 22 | topic="refs/heads/$2" 23 | else 24 | topic=`git symbolic-ref HEAD` || 25 | exit 0 ;# we do not interrupt rebasing detached HEAD 26 | fi 27 | 28 | case "$topic" in 29 | refs/heads/??/*) 30 | ;; 31 | *) 32 | exit 0 ;# we do not interrupt others. 33 | ;; 34 | esac 35 | 36 | # Now we are dealing with a topic branch being rebased 37 | # on top of master. Is it OK to rebase it? 38 | 39 | # Does the topic really exist? 40 | git show-ref -q "$topic" || { 41 | echo >&2 "No such branch $topic" 42 | exit 1 43 | } 44 | 45 | # Is topic fully merged to master? 46 | not_in_master=`git rev-list --pretty=oneline ^master "$topic"` 47 | if test -z "$not_in_master" 48 | then 49 | echo >&2 "$topic is fully merged to master; better remove it." 50 | exit 1 ;# we could allow it, but there is no point. 51 | fi 52 | 53 | # Is topic ever merged to next? If so you should not be rebasing it. 54 | only_next_1=`git rev-list ^master "^$topic" ${publish} | sort` 55 | only_next_2=`git rev-list ^master ${publish} | sort` 56 | if test "$only_next_1" = "$only_next_2" 57 | then 58 | not_in_topic=`git rev-list "^$topic" master` 59 | if test -z "$not_in_topic" 60 | then 61 | echo >&2 "$topic is already up to date with master" 62 | exit 1 ;# we could allow it, but there is no point. 63 | else 64 | exit 0 65 | fi 66 | else 67 | not_in_next=`git rev-list --pretty=oneline ^${publish} "$topic"` 68 | /usr/bin/perl -e ' 69 | my $topic = $ARGV[0]; 70 | my $msg = "* $topic has commits already merged to public branch:\n"; 71 | my (%not_in_next) = map { 72 | /^([0-9a-f]+) /; 73 | ($1 => 1); 74 | } split(/\n/, $ARGV[1]); 75 | for my $elem (map { 76 | /^([0-9a-f]+) (.*)$/; 77 | [$1 => $2]; 78 | } split(/\n/, $ARGV[2])) { 79 | if (!exists $not_in_next{$elem->[0]}) { 80 | if ($msg) { 81 | print STDERR $msg; 82 | undef $msg; 83 | } 84 | print STDERR " $elem->[1]\n"; 85 | } 86 | } 87 | ' "$topic" "$not_in_next" "$not_in_master" 88 | exit 1 89 | fi 90 | 91 | <<\DOC_END 92 | 93 | This sample hook safeguards topic branches that have been 94 | published from being rewound. 95 | 96 | The workflow assumed here is: 97 | 98 | * Once a topic branch forks from "master", "master" is never 99 | merged into it again (either directly or indirectly). 100 | 101 | * Once a topic branch is fully cooked and merged into "master", 102 | it is deleted. If you need to build on top of it to correct 103 | earlier mistakes, a new topic branch is created by forking at 104 | the tip of the "master". This is not strictly necessary, but 105 | it makes it easier to keep your history simple. 106 | 107 | * Whenever you need to test or publish your changes to topic 108 | branches, merge them into "next" branch. 109 | 110 | The script, being an example, hardcodes the publish branch name 111 | to be "next", but it is trivial to make it configurable via 112 | $GIT_DIR/config mechanism. 113 | 114 | With this workflow, you would want to know: 115 | 116 | (1) ... if a topic branch has ever been merged to "next". Young 117 | topic branches can have stupid mistakes you would rather 118 | clean up before publishing, and things that have not been 119 | merged into other branches can be easily rebased without 120 | affecting other people. But once it is published, you would 121 | not want to rewind it. 122 | 123 | (2) ... if a topic branch has been fully merged to "master". 124 | Then you can delete it. More importantly, you should not 125 | build on top of it -- other people may already want to 126 | change things related to the topic as patches against your 127 | "master", so if you need further changes, it is better to 128 | fork the topic (perhaps with the same name) afresh from the 129 | tip of "master". 130 | 131 | Let's look at this example: 132 | 133 | o---o---o---o---o---o---o---o---o---o "next" 134 | / / / / 135 | / a---a---b A / / 136 | / / / / 137 | / / c---c---c---c B / 138 | / / / \ / 139 | / / / b---b C \ / 140 | / / / / \ / 141 | ---o---o---o---o---o---o---o---o---o---o---o "master" 142 | 143 | 144 | A, B and C are topic branches. 145 | 146 | * A has one fix since it was merged up to "next". 147 | 148 | * B has finished. It has been fully merged up to "master" and "next", 149 | and is ready to be deleted. 150 | 151 | * C has not merged to "next" at all. 152 | 153 | We would want to allow C to be rebased, refuse A, and encourage 154 | B to be deleted. 155 | 156 | To compute (1): 157 | 158 | git rev-list ^master ^topic next 159 | git rev-list ^master next 160 | 161 | if these match, topic has not merged in next at all. 162 | 163 | To compute (2): 164 | 165 | git rev-list master..topic 166 | 167 | if this is empty, it is fully merged to "master". 168 | 169 | DOC_END 170 | -------------------------------------------------------------------------------- /internal/integration/testdata/remote/git-internals-fork.git/hooks/pre-receive.sample: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # An example hook script to make use of push options. 4 | # The example simply echoes all push options that start with 'echoback=' 5 | # and rejects all pushes when the "reject" push option is used. 6 | # 7 | # To enable this hook, rename this file to "pre-receive". 8 | 9 | if test -n "$GIT_PUSH_OPTION_COUNT" 10 | then 11 | i=0 12 | while test "$i" -lt "$GIT_PUSH_OPTION_COUNT" 13 | do 14 | eval "value=\$GIT_PUSH_OPTION_$i" 15 | case "$value" in 16 | echoback=*) 17 | echo "echo from the pre-receive-hook: ${value#*=}" >&2 18 | ;; 19 | reject) 20 | exit 1 21 | esac 22 | i=$((i + 1)) 23 | done 24 | fi 25 | -------------------------------------------------------------------------------- /internal/integration/testdata/remote/git-internals-fork.git/hooks/prepare-commit-msg.sample: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # An example hook script to prepare the commit log message. 4 | # Called by "git commit" with the name of the file that has the 5 | # commit message, followed by the description of the commit 6 | # message's source. The hook's purpose is to edit the commit 7 | # message file. If the hook fails with a non-zero status, 8 | # the commit is aborted. 9 | # 10 | # To enable this hook, rename this file to "prepare-commit-msg". 11 | 12 | # This hook includes three examples. The first one removes the 13 | # "# Please enter the commit message..." help message. 14 | # 15 | # The second includes the output of "git diff --name-status -r" 16 | # into the message, just before the "git status" output. It is 17 | # commented because it doesn't cope with --amend or with squashed 18 | # commits. 19 | # 20 | # The third example adds a Signed-off-by line to the message, that can 21 | # still be edited. This is rarely a good idea. 22 | 23 | COMMIT_MSG_FILE=$1 24 | COMMIT_SOURCE=$2 25 | SHA1=$3 26 | 27 | /usr/bin/perl -i.bak -ne 'print unless(m/^. Please enter the commit message/..m/^#$/)' "$COMMIT_MSG_FILE" 28 | 29 | # case "$COMMIT_SOURCE,$SHA1" in 30 | # ,|template,) 31 | # /usr/bin/perl -i.bak -pe ' 32 | # print "\n" . `git diff --cached --name-status -r` 33 | # if /^#/ && $first++ == 0' "$COMMIT_MSG_FILE" ;; 34 | # *) ;; 35 | # esac 36 | 37 | # SOB=$(git var GIT_COMMITTER_IDENT | sed -n 's/^\(.*>\).*$/Signed-off-by: \1/p') 38 | # git interpret-trailers --in-place --trailer "$SOB" "$COMMIT_MSG_FILE" 39 | # if test -z "$COMMIT_SOURCE" 40 | # then 41 | # /usr/bin/perl -i.bak -pe 'print "\n" if !$first_line++' "$COMMIT_MSG_FILE" 42 | # fi 43 | -------------------------------------------------------------------------------- /internal/integration/testdata/remote/git-internals-fork.git/hooks/push-to-checkout.sample: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # An example hook script to update a checked-out tree on a git push. 4 | # 5 | # This hook is invoked by git-receive-pack(1) when it reacts to git 6 | # push and updates reference(s) in its repository, and when the push 7 | # tries to update the branch that is currently checked out and the 8 | # receive.denyCurrentBranch configuration variable is set to 9 | # updateInstead. 10 | # 11 | # By default, such a push is refused if the working tree and the index 12 | # of the remote repository has any difference from the currently 13 | # checked out commit; when both the working tree and the index match 14 | # the current commit, they are updated to match the newly pushed tip 15 | # of the branch. This hook is to be used to override the default 16 | # behaviour; however the code below reimplements the default behaviour 17 | # as a starting point for convenient modification. 18 | # 19 | # The hook receives the commit with which the tip of the current 20 | # branch is going to be updated: 21 | commit=$1 22 | 23 | # It can exit with a non-zero status to refuse the push (when it does 24 | # so, it must not modify the index or the working tree). 25 | die () { 26 | echo >&2 "$*" 27 | exit 1 28 | } 29 | 30 | # Or it can make any necessary changes to the working tree and to the 31 | # index to bring them to the desired state when the tip of the current 32 | # branch is updated to the new commit, and exit with a zero status. 33 | # 34 | # For example, the hook can simply run git read-tree -u -m HEAD "$1" 35 | # in order to emulate git fetch that is run in the reverse direction 36 | # with git push, as the two-tree form of git read-tree -u -m is 37 | # essentially the same as git switch or git checkout that switches 38 | # branches while keeping the local changes in the working tree that do 39 | # not interfere with the difference between the branches. 40 | 41 | # The below is a more-or-less exact translation to shell of the C code 42 | # for the default behaviour for git's push-to-checkout hook defined in 43 | # the push_to_deploy() function in builtin/receive-pack.c. 44 | # 45 | # Note that the hook will be executed from the repository directory, 46 | # not from the working tree, so if you want to perform operations on 47 | # the working tree, you will have to adapt your code accordingly, e.g. 48 | # by adding "cd .." or using relative paths. 49 | 50 | if ! git update-index -q --ignore-submodules --refresh 51 | then 52 | die "Up-to-date check failed" 53 | fi 54 | 55 | if ! git diff-files --quiet --ignore-submodules -- 56 | then 57 | die "Working directory has unstaged changes" 58 | fi 59 | 60 | # This is a rough translation of: 61 | # 62 | # head_has_history() ? "HEAD" : EMPTY_TREE_SHA1_HEX 63 | if git cat-file -e HEAD 2>/dev/null 64 | then 65 | head=HEAD 66 | else 67 | head=$(git hash-object -t tree --stdin &2 35 | echo " (if you want, you could supply GIT_DIR then run" >&2 36 | echo " $0 )" >&2 37 | exit 1 38 | fi 39 | 40 | if [ -z "$refname" -o -z "$oldrev" -o -z "$newrev" ]; then 41 | echo "usage: $0 " >&2 42 | exit 1 43 | fi 44 | 45 | # --- Config 46 | allowunannotated=$(git config --type=bool hooks.allowunannotated) 47 | allowdeletebranch=$(git config --type=bool hooks.allowdeletebranch) 48 | denycreatebranch=$(git config --type=bool hooks.denycreatebranch) 49 | allowdeletetag=$(git config --type=bool hooks.allowdeletetag) 50 | allowmodifytag=$(git config --type=bool hooks.allowmodifytag) 51 | 52 | # check for no description 53 | projectdesc=$(sed -e '1q' "$GIT_DIR/description") 54 | case "$projectdesc" in 55 | "Unnamed repository"* | "") 56 | echo "*** Project description file hasn't been set" >&2 57 | exit 1 58 | ;; 59 | esac 60 | 61 | # --- Check types 62 | # if $newrev is 0000...0000, it's a commit to delete a ref. 63 | zero=$(git hash-object --stdin &2 76 | echo "*** Use 'git tag [ -a | -s ]' for tags you want to propagate." >&2 77 | exit 1 78 | fi 79 | ;; 80 | refs/tags/*,delete) 81 | # delete tag 82 | if [ "$allowdeletetag" != "true" ]; then 83 | echo "*** Deleting a tag is not allowed in this repository" >&2 84 | exit 1 85 | fi 86 | ;; 87 | refs/tags/*,tag) 88 | # annotated tag 89 | if [ "$allowmodifytag" != "true" ] && git rev-parse $refname > /dev/null 2>&1 90 | then 91 | echo "*** Tag '$refname' already exists." >&2 92 | echo "*** Modifying a tag is not allowed in this repository." >&2 93 | exit 1 94 | fi 95 | ;; 96 | refs/heads/*,commit) 97 | # branch 98 | if [ "$oldrev" = "$zero" -a "$denycreatebranch" = "true" ]; then 99 | echo "*** Creating a branch is not allowed in this repository" >&2 100 | exit 1 101 | fi 102 | ;; 103 | refs/heads/*,delete) 104 | # delete branch 105 | if [ "$allowdeletebranch" != "true" ]; then 106 | echo "*** Deleting a branch is not allowed in this repository" >&2 107 | exit 1 108 | fi 109 | ;; 110 | refs/remotes/*,commit) 111 | # tracking branch 112 | ;; 113 | refs/remotes/*,delete) 114 | # delete tracking branch 115 | if [ "$allowdeletebranch" != "true" ]; then 116 | echo "*** Deleting a tracking branch is not allowed in this repository" >&2 117 | exit 1 118 | fi 119 | ;; 120 | *) 121 | # Anything else (is there anything else?) 122 | echo "*** Update hook: unknown type of update to ref $refname of type $newrev_type" >&2 123 | exit 1 124 | ;; 125 | esac 126 | 127 | # --- Finished 128 | exit 0 129 | -------------------------------------------------------------------------------- /internal/integration/testdata/remote/git-internals-fork.git/info/exclude: -------------------------------------------------------------------------------- 1 | # git ls-files --others --exclude-from=.git/info/exclude 2 | # Lines that start with '#' are comments. 3 | # For a project mostly in C, the following would be a good set of 4 | # exclude patterns (uncomment them if you want to use them): 5 | # *.[oa] 6 | # *~ 7 | -------------------------------------------------------------------------------- /internal/integration/testdata/remote/git-internals-fork.git/objects/info/alternates: -------------------------------------------------------------------------------- 1 | ../../network.git/objects 2 | -------------------------------------------------------------------------------- /internal/integration/testdata/remote/git-internals-fork.git/packed-refs: -------------------------------------------------------------------------------- 1 | # pack-refs with: peeled fully-peeled sorted 2 | 4f37ac0f4282b2ac78e9242f2b4d570e23d6552c refs/heads/branch-1 3 | 9ae7a27e1095e1e20c099c5bff79c3725825eb6b refs/heads/branch-2 4 | e589bdee50e39beac56220c4b7a716225f79e3cf refs/heads/main 5 | -------------------------------------------------------------------------------- /internal/integration/testdata/remote/git-internals-fork.git/refs/heads/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/spokes-receive-pack/acac8763c60f636c44baaf5c3887895cf5f55c30/internal/integration/testdata/remote/git-internals-fork.git/refs/heads/.gitkeep -------------------------------------------------------------------------------- /internal/integration/testdata/remote/git-internals-fork.git/refs/tags/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/spokes-receive-pack/acac8763c60f636c44baaf5c3887895cf5f55c30/internal/integration/testdata/remote/git-internals-fork.git/refs/tags/.gitkeep -------------------------------------------------------------------------------- /internal/integration/testdata/remote/git-internals.git/HEAD: -------------------------------------------------------------------------------- 1 | ref: refs/heads/main 2 | -------------------------------------------------------------------------------- /internal/integration/testdata/remote/git-internals.git/config: -------------------------------------------------------------------------------- 1 | [core] 2 | repositoryformatversion = 0 3 | filemode = true 4 | bare = true 5 | ignorecase = true 6 | precomposeunicode = true 7 | -------------------------------------------------------------------------------- /internal/integration/testdata/remote/git-internals.git/objects/info/alternates: -------------------------------------------------------------------------------- 1 | ../../network.git/objects 2 | -------------------------------------------------------------------------------- /internal/integration/testdata/remote/git-internals.git/packed-refs: -------------------------------------------------------------------------------- 1 | # pack-refs with: peeled fully-peeled sorted 2 | 4f37ac0f4282b2ac78e9242f2b4d570e23d6552c refs/heads/branch-1 3 | 9ae7a27e1095e1e20c099c5bff79c3725825eb6b refs/heads/branch-2 4 | e589bdee50e39beac56220c4b7a716225f79e3cf refs/heads/main 5 | -------------------------------------------------------------------------------- /internal/integration/testdata/remote/network.git/HEAD: -------------------------------------------------------------------------------- 1 | ref: refs/heads/main 2 | -------------------------------------------------------------------------------- /internal/integration/testdata/remote/network.git/config: -------------------------------------------------------------------------------- 1 | [core] 2 | repositoryformatversion = 0 3 | filemode = true 4 | bare = true 5 | ignorecase = true 6 | precomposeunicode = true 7 | logallrefupdates = false 8 | [transfer] 9 | unpacklimit = 1 10 | -------------------------------------------------------------------------------- /internal/integration/testdata/remote/network.git/description: -------------------------------------------------------------------------------- 1 | git-nw shared network.git repository 2 | -------------------------------------------------------------------------------- /internal/integration/testdata/remote/network.git/objects/05/9ea99a3c9151f96b2a4f99ea9604a4fff99306: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/spokes-receive-pack/acac8763c60f636c44baaf5c3887895cf5f55c30/internal/integration/testdata/remote/network.git/objects/05/9ea99a3c9151f96b2a4f99ea9604a4fff99306 -------------------------------------------------------------------------------- /internal/integration/testdata/remote/network.git/objects/13/50f603d9d4712f9219861291e874702427e455: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/spokes-receive-pack/acac8763c60f636c44baaf5c3887895cf5f55c30/internal/integration/testdata/remote/network.git/objects/13/50f603d9d4712f9219861291e874702427e455 -------------------------------------------------------------------------------- /internal/integration/testdata/remote/network.git/objects/3a/a69c529d9a10c64443ec36e7e3599ac60680ab: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/spokes-receive-pack/acac8763c60f636c44baaf5c3887895cf5f55c30/internal/integration/testdata/remote/network.git/objects/3a/a69c529d9a10c64443ec36e7e3599ac60680ab -------------------------------------------------------------------------------- /internal/integration/testdata/remote/network.git/objects/4f/37ac0f4282b2ac78e9242f2b4d570e23d6552c: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/spokes-receive-pack/acac8763c60f636c44baaf5c3887895cf5f55c30/internal/integration/testdata/remote/network.git/objects/4f/37ac0f4282b2ac78e9242f2b4d570e23d6552c -------------------------------------------------------------------------------- /internal/integration/testdata/remote/network.git/objects/81/125cd7d188fc6c83fda2a97d0f3f6267713532: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/spokes-receive-pack/acac8763c60f636c44baaf5c3887895cf5f55c30/internal/integration/testdata/remote/network.git/objects/81/125cd7d188fc6c83fda2a97d0f3f6267713532 -------------------------------------------------------------------------------- /internal/integration/testdata/remote/network.git/objects/96/9206b2584b8293d677b3f7bbdc6017b8c573f6: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/spokes-receive-pack/acac8763c60f636c44baaf5c3887895cf5f55c30/internal/integration/testdata/remote/network.git/objects/96/9206b2584b8293d677b3f7bbdc6017b8c573f6 -------------------------------------------------------------------------------- /internal/integration/testdata/remote/network.git/objects/9a/e7a27e1095e1e20c099c5bff79c3725825eb6b: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/spokes-receive-pack/acac8763c60f636c44baaf5c3887895cf5f55c30/internal/integration/testdata/remote/network.git/objects/9a/e7a27e1095e1e20c099c5bff79c3725825eb6b -------------------------------------------------------------------------------- /internal/integration/testdata/remote/network.git/objects/9d/6d97ef453d420f2b7e3a77b0a68ff1035f78f1: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/spokes-receive-pack/acac8763c60f636c44baaf5c3887895cf5f55c30/internal/integration/testdata/remote/network.git/objects/9d/6d97ef453d420f2b7e3a77b0a68ff1035f78f1 -------------------------------------------------------------------------------- /internal/integration/testdata/remote/network.git/objects/a3/668948e211f5dffc4a66109b97fd23693531ae: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/spokes-receive-pack/acac8763c60f636c44baaf5c3887895cf5f55c30/internal/integration/testdata/remote/network.git/objects/a3/668948e211f5dffc4a66109b97fd23693531ae -------------------------------------------------------------------------------- /internal/integration/testdata/remote/network.git/objects/d7/2ffbc4a701ecb9580058cfddc1d63e0b5ad817: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/spokes-receive-pack/acac8763c60f636c44baaf5c3887895cf5f55c30/internal/integration/testdata/remote/network.git/objects/d7/2ffbc4a701ecb9580058cfddc1d63e0b5ad817 -------------------------------------------------------------------------------- /internal/integration/testdata/remote/network.git/objects/e5/89bdee50e39beac56220c4b7a716225f79e3cf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/spokes-receive-pack/acac8763c60f636c44baaf5c3887895cf5f55c30/internal/integration/testdata/remote/network.git/objects/e5/89bdee50e39beac56220c4b7a716225f79e3cf -------------------------------------------------------------------------------- /internal/integration/testdata/remote/network.git/objects/e8/6fe46ef42e3f721af78091d77cff13d95f9db3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/spokes-receive-pack/acac8763c60f636c44baaf5c3887895cf5f55c30/internal/integration/testdata/remote/network.git/objects/e8/6fe46ef42e3f721af78091d77cff13d95f9db3 -------------------------------------------------------------------------------- /internal/integration/testdata/remote/network.git/objects/info/commit-graphs/commit-graph-chain: -------------------------------------------------------------------------------- 1 | ebb808245825422c9c8704c510ec9118e52e24cd 2 | -------------------------------------------------------------------------------- /internal/integration/testdata/remote/network.git/objects/info/commit-graphs/graph-ebb808245825422c9c8704c510ec9118e52e24cd.graph: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/spokes-receive-pack/acac8763c60f636c44baaf5c3887895cf5f55c30/internal/integration/testdata/remote/network.git/objects/info/commit-graphs/graph-ebb808245825422c9c8704c510ec9118e52e24cd.graph -------------------------------------------------------------------------------- /internal/integration/testdata/remote/network.git/refs/remotes/git-internals-fork/heads/branch-1: -------------------------------------------------------------------------------- 1 | 4f37ac0f4282b2ac78e9242f2b4d570e23d6552c 2 | -------------------------------------------------------------------------------- /internal/integration/testdata/remote/network.git/refs/remotes/git-internals-fork/heads/branch-2: -------------------------------------------------------------------------------- 1 | 9ae7a27e1095e1e20c099c5bff79c3725825eb6b 2 | -------------------------------------------------------------------------------- /internal/integration/testdata/remote/network.git/refs/remotes/git-internals-fork/heads/main: -------------------------------------------------------------------------------- 1 | e589bdee50e39beac56220c4b7a716225f79e3cf 2 | -------------------------------------------------------------------------------- /internal/integration/testdata/remote/network.git/refs/remotes/git-internals/heads/branch-1: -------------------------------------------------------------------------------- 1 | 4f37ac0f4282b2ac78e9242f2b4d570e23d6552c 2 | -------------------------------------------------------------------------------- /internal/integration/testdata/remote/network.git/refs/remotes/git-internals/heads/branch-2: -------------------------------------------------------------------------------- 1 | 9ae7a27e1095e1e20c099c5bff79c3725825eb6b 2 | -------------------------------------------------------------------------------- /internal/integration/testdata/remote/network.git/refs/remotes/git-internals/heads/main: -------------------------------------------------------------------------------- 1 | e589bdee50e39beac56220c4b7a716225f79e3cf 2 | -------------------------------------------------------------------------------- /internal/integration/testdata/set-up-bad-date-push: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | #/ Usage: set-up-bad-date-push 3 | #/ 4 | #!!! This script shouldn't need run unless we need to make changes to the 5 | #!!! generated Git data. You should check in the contents of 6 | #!!! internal/integration/testdata/bad-date.git after running this script. 7 | #/ 8 | #/ Outputs: 9 | #/ internal/integration/testdata/bad-date.git 10 | #/ - Repository with commits that include malformed dates 11 | 12 | set -e 13 | set -o nounset 14 | set -o pipefail 15 | 16 | DEFAULT_BRANCH=main 17 | 18 | set -e 19 | set -o nounset 20 | 21 | make_repo() { 22 | local object_format="$1" 23 | local dest_repo="$2" 24 | 25 | ( 26 | set -e 27 | set -o nounset 28 | set -x 29 | 30 | cd "$(dirname "$0")" 31 | rm -rf "$dest_repo" 32 | git init --bare --quiet \ 33 | --object-format "$object_format" \ 34 | --initial-branch $DEFAULT_BRANCH \ 35 | "$dest_repo" 36 | cd "$dest_repo" 37 | 38 | rm -r hooks info 39 | 40 | #/fi/ commit refs/heads/__BRANCH__ 41 | #/fi/ committer Hubot 1681399000 +1200 42 | #/fi/ data < @boom> 1571755547 +0000\n' 57 | printf 'committer Mona Lisa @boom> 1571755547 +0000\n' 58 | printf '\n' 59 | printf 'bad commit\n' 60 | ) | git hash-object -t commit --stdin -w 61 | )" 62 | git update-ref refs/heads/$DEFAULT_BRANCH $bad_date_commit 63 | 64 | git repack -adf 65 | git pack-refs --all 66 | # Make sure Git doesn't delete the refs dir. 67 | touch refs/.keep 68 | ) 69 | } 70 | 71 | summary() { 72 | local dest_repo="$1" 73 | ( 74 | set -e 75 | set -o nounset 76 | cd "$(dirname "$0")/$dest_repo" 77 | echo -n "dir: " 78 | pwd 79 | echo -n "obj format: " 80 | git rev-parse --show-object-format 81 | echo -n "tip commit: " 82 | git rev-parse HEAD 83 | ) 84 | } 85 | 86 | make_repo sha1 bad-date/sha1.git 87 | make_repo sha256 bad-date/sha256.git 88 | 89 | echo ========= 90 | summary bad-date/sha1.git 91 | echo ========= 92 | summary bad-date/sha256.git 93 | echo ========= 94 | 95 | echo DONE 96 | -------------------------------------------------------------------------------- /internal/integration/testdata/set-up-missing-objects-push: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | #/ Usage: set-up-missing-objects-push 3 | #/ 4 | #!!! This script shouldn't need run unless we need to make changes 5 | #!!! to the generated Git data. You should check in the contents of 6 | #!!! internal/integration/testdata/missing-push after running this script. 7 | #/ 8 | #/ Sets up a scenario where we can make a push that's missing objects. 9 | #/ 10 | #/ Pack -> C 11 | #/ | 12 | #/ Missing -> B 13 | #/ | 14 | #/ Remote -> A 15 | #/ 16 | #/ Outputs: 17 | #/ internal/integration/testdata/missing-objects/remote.git 18 | #/ - Valid repository with A as its default ("example") and "other" branches. 19 | #/ internal/integration/testdata/missing-objects/info.json 20 | #/ - oid of A, oid of C, name of ref to update. 21 | #/ internal/integration/testdata/missing-objects/bad.pack 22 | #/ - packfile that only contains commit C. 23 | 24 | set -e 25 | set -o nounset 26 | set -o pipefail 27 | 28 | DEFAULT_BRANCH=example 29 | EXTRA_BRANCH=other 30 | 31 | set -x 32 | 33 | cd "$(dirname "$0")" 34 | rm -rf missing-objects 35 | mkdir missing-objects 36 | cd missing-objects 37 | 38 | git init --bare --quiet -b $DEFAULT_BRANCH remote.git 39 | rm -rf remote.git/hooks remote.git/info/exclude 40 | 41 | git init --bare --quiet -b $DEFAULT_BRANCH work.git 42 | cd work.git 43 | 44 | #/fi/ commit refs/heads/__BRANCH__ 45 | #/fi/ committer Hubot 1681399000 +1200 46 | #/fi/ data < 1681399010 +1200 52 | #/fi/ data < 1681399020 +1200 58 | #/fi/ data < C. 73 | printf '{"push_from":"%s","push_to":"%s","ref":"refs/heads/%s","extra_ref":"refs/heads/%s"}' "$COMMIT_A" "$COMMIT_C" "$DEFAULT_BRANCH" "$EXTRA_BRANCH" \ 74 | | jq . | tee ../info.json 75 | 76 | # Only include commit C in the pack. 77 | # pack-objects should output "Total 1" (objects packed). 78 | # You can verify this by running 'git index-pack bad.pack' and 79 | # 'git verify-pack -v bad.idx'. 80 | printf "^%s\n%s\n" "$COMMIT_B" "$COMMIT_C" \ 81 | | git pack-objects --revs --stdout >../bad.pack 82 | 83 | # Make an empty pack. 84 | git pack-objects --stdout ../empty.pack 85 | 86 | cd .. 87 | rm -rf work.git 88 | 89 | cd remote.git 90 | git repack -adf 91 | git pack-refs --all 92 | # Make sure Git doesn't delete the refs dir. 93 | touch refs/.keep 94 | # This should only show commit A. 95 | git --no-pager log example --graph --all 96 | 97 | cd .. 98 | find . -type f -ls 99 | 100 | echo DONE 101 | -------------------------------------------------------------------------------- /internal/integration/util.go: -------------------------------------------------------------------------------- 1 | package integration 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "os/exec" 7 | ) 8 | 9 | func RunMain(env []string) error { 10 | command := exec.Command("spokes-receive-pack", os.Args[1:]...) 11 | command.Env = append( 12 | os.Environ(), 13 | env..., 14 | ) 15 | command.Stdout = os.Stdout 16 | command.Stdin = os.Stdin 17 | command.Stderr = os.Stderr 18 | 19 | if err := command.Run(); err != nil { 20 | fmt.Fprintf(os.Stderr, "unexpected error running the spokes-receive-pack binary. Error: %s", err.Error()) 21 | return err 22 | } 23 | 24 | return nil 25 | } 26 | -------------------------------------------------------------------------------- /internal/objectformat/git.go: -------------------------------------------------------------------------------- 1 | package objectformat 2 | 3 | import ( 4 | "fmt" 5 | "os/exec" 6 | "strings" 7 | ) 8 | 9 | const ( 10 | NullOIDSHA1 = "0000000000000000000000000000000000000000" 11 | NullOIDSHA256 = "0000000000000000000000000000000000000000000000000000000000000000" 12 | ) 13 | 14 | type ObjectFormat string 15 | 16 | // GetObjectFormat returns the object format for the repo located at repo. 17 | func GetObjectFormat(repo string) (ObjectFormat, error) { 18 | cmd := exec.Command( 19 | "git", 20 | "rev-parse", 21 | "--show-object-format", 22 | ) 23 | cmd.Dir = repo 24 | 25 | out, err := cmd.Output() 26 | if err != nil { 27 | return "", fmt.Errorf("reading git object format: %w", err) 28 | } 29 | 30 | value := strings.TrimSpace(string(out)) 31 | switch value { 32 | case "sha1", "sha256": 33 | return ObjectFormat(value), nil 34 | default: 35 | return "", fmt.Errorf("unknown object format: %s", value) 36 | } 37 | } 38 | 39 | func (of ObjectFormat) NullOID() string { 40 | switch of { 41 | case "sha256": 42 | return NullOIDSHA256 43 | default: 44 | return NullOIDSHA1 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /internal/objectformat/git_test.go: -------------------------------------------------------------------------------- 1 | package objectformat 2 | 3 | import ( 4 | "regexp" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestNullOID(t *testing.T) { 11 | nullRE := regexp.MustCompile(`\A0+\z`) 12 | 13 | sha1 := ObjectFormat("sha1") 14 | require.Equal(t, len(sha1.NullOID()), 40) 15 | require.Regexp(t, nullRE, sha1.NullOID()) 16 | 17 | sha256 := ObjectFormat("sha256") 18 | require.Equal(t, len(sha256.NullOID()), 64) 19 | require.Regexp(t, nullRE, sha256.NullOID()) 20 | } 21 | 22 | func TestGetObjectFormat(t *testing.T) { 23 | of, err := GetObjectFormat("../spokes/testdata/lots-of-refs.git") 24 | require.NoError(t, err) 25 | require.Equal(t, of, ObjectFormat("sha1")) 26 | } 27 | -------------------------------------------------------------------------------- /internal/pktline/capabilities.go: -------------------------------------------------------------------------------- 1 | package pktline 2 | 3 | import ( 4 | "fmt" 5 | "sort" 6 | "strings" 7 | ) 8 | 9 | const ( 10 | MultiAck = "multi_ack" 11 | MultiAckDetailed = "multi_ack_detailed" 12 | NoDone = "no-done" 13 | ThinPack = "thin-pack" 14 | SideBand = "side-band" 15 | SideBand64k = "side-band-64k" 16 | OfsDelta = "ofs-delta" 17 | Agent = "agent" 18 | ObjectFormat = "object-format" 19 | Symref = "symref" 20 | Shallow = "shallow" 21 | DeepenSince = "deepen-since" 22 | DeepenNot = "deepen-not" 23 | DeepenRelative = "deepen-relative" 24 | NoProgress = "no-progress" 25 | IncludeTag = "include-tag" 26 | ReportStatus = "report-status" 27 | ReportStatusV2 = "report-status-v2" 28 | DeleteRefs = "delete-refs" 29 | Quiet = "quiet" 30 | Atomic = "atomic" 31 | PushOptions = "push-options" 32 | AllowTipSha1InWant = "allow-tip-sha1-in-want" 33 | AllowReachableSha1InWant = "allow-reachable-sha1-in-want" 34 | PushCert = "push-cert" 35 | Filter = "filter" 36 | SessionId = "session-id" 37 | ) 38 | 39 | type Capability struct { 40 | name string 41 | value string 42 | } 43 | 44 | func newCapability(data string) (Capability, error) { 45 | rawCap := strings.Split(data, "=") 46 | cap := Capability{} 47 | switch len(rawCap) { 48 | case 1: 49 | cap.name = rawCap[0] 50 | case 2: 51 | cap.name = rawCap[0] 52 | cap.value = rawCap[1] 53 | default: 54 | return Capability{}, fmt.Errorf("unexpected Capability format %s", data) 55 | } 56 | 57 | return cap, nil 58 | } 59 | 60 | func (c Capability) Name() string { 61 | return c.name 62 | } 63 | 64 | func (c Capability) Value() string { 65 | return c.value 66 | } 67 | 68 | // Capabilities models the capabilities that can be sent across the client and the server in the pack protocol V1 69 | // The abstraction parses all the capabilities defined in the spec but our goal is to focus in those relevant 70 | // for the upload process part 71 | type Capabilities struct { 72 | caps map[string]Capability 73 | } 74 | 75 | // ParseCapabilities converts the passed capabilities (as received in the protocol) into its corresponding typed object 76 | func ParseCapabilities(capabilities []byte) (Capabilities, error) { 77 | caps := string(capabilities) 78 | caps = strings.TrimSuffix(caps, "\n") 79 | splitted := strings.Split(caps, " ") 80 | 81 | parsedCaps := make(map[string]Capability, len(caps)) 82 | for _, c := range splitted { 83 | cap, err := newCapability(c) 84 | if err != nil { 85 | return Capabilities{}, fmt.Errorf("unable to parse Capability %s", c) 86 | } 87 | parsedCaps[cap.Name()] = cap 88 | } 89 | 90 | return Capabilities{caps: parsedCaps}, nil 91 | } 92 | 93 | func (c Capabilities) MultiAck() Capability { 94 | return c.caps[MultiAck] 95 | } 96 | func (c Capabilities) MultiAckDetailed() Capability { 97 | return c.caps[MultiAckDetailed] 98 | } 99 | func (c Capabilities) NoDone() Capability { 100 | return c.caps[NoDone] 101 | } 102 | func (c Capabilities) ThinPack() Capability { 103 | return c.caps[ThinPack] 104 | } 105 | func (c Capabilities) SideBand() Capability { 106 | return c.caps[SideBand] 107 | } 108 | func (c Capabilities) SideBand64k() Capability { 109 | return c.caps[SideBand64k] 110 | } 111 | func (c Capabilities) OfsDelta() Capability { 112 | return c.caps[OfsDelta] 113 | } 114 | func (c Capabilities) Agent() Capability { 115 | return c.caps[Agent] 116 | } 117 | func (c Capabilities) ObjectFormat() Capability { 118 | return c.caps[ObjectFormat] 119 | } 120 | func (c Capabilities) Symref() Capability { 121 | return c.caps[Symref] 122 | } 123 | func (c Capabilities) Shallow() Capability { 124 | return c.caps[Shallow] 125 | } 126 | func (c Capabilities) DeepenSince() Capability { 127 | return c.caps[DeepenSince] 128 | } 129 | func (c Capabilities) DeepenNot() Capability { 130 | return c.caps[DeepenNot] 131 | } 132 | func (c Capabilities) DeepenRelative() Capability { 133 | return c.caps[DeepenRelative] 134 | } 135 | func (c Capabilities) NoProgress() Capability { 136 | return c.caps[NoProgress] 137 | } 138 | func (c Capabilities) IncludeTag() Capability { 139 | return c.caps[IncludeTag] 140 | } 141 | func (c Capabilities) ReportStatus() Capability { 142 | return c.caps[ReportStatus] 143 | } 144 | func (c Capabilities) ReportStatusV2() Capability { 145 | return c.caps[ReportStatusV2] 146 | } 147 | func (c Capabilities) DeleteRefs() Capability { 148 | return c.caps[DeleteRefs] 149 | } 150 | func (c Capabilities) Quiet() Capability { 151 | return c.caps[Quiet] 152 | } 153 | func (c Capabilities) Atomic() Capability { 154 | return c.caps[Atomic] 155 | } 156 | func (c Capabilities) PushOptions() Capability { 157 | return c.caps[PushOptions] 158 | } 159 | func (c Capabilities) AllowTipSha1InWant() Capability { 160 | return c.caps[AllowTipSha1InWant] 161 | } 162 | func (c Capabilities) AllowReachableSha1InWant() Capability { 163 | return c.caps[AllowReachableSha1InWant] 164 | } 165 | func (c Capabilities) PushCert() Capability { 166 | return c.caps[PushCert] 167 | } 168 | func (c Capabilities) Filter() Capability { 169 | return c.caps[Filter] 170 | } 171 | func (c Capabilities) SessionId() Capability { 172 | return c.caps[SessionId] 173 | } 174 | 175 | func (c Capabilities) Names() []string { 176 | res := make([]string, 0, len(c.caps)) 177 | for k := range c.caps { 178 | res = append(res, k) 179 | } 180 | sort.Strings(res) 181 | return res 182 | } 183 | 184 | func (c Capabilities) Get(cap string) (Capability, bool) { 185 | capability, found := c.caps[cap] 186 | return capability, found 187 | } 188 | 189 | func (c Capabilities) IsDefined(cap string) bool { 190 | _, found := c.caps[cap] 191 | return found 192 | } 193 | 194 | func IsSafeCapabilityValue(val string) bool { 195 | // Git needs this not to include \r, \n, \t, or ' '. 196 | // https://github.com/git/git/blob/d7d8841f67f29e6ecbad85a11805c907d0f00d5d/connect.c#L629 197 | for _, b := range []byte(val) { 198 | switch b { 199 | case ' ', '\r', '\n', '\t': 200 | return false 201 | } 202 | } 203 | return true 204 | } 205 | -------------------------------------------------------------------------------- /internal/pktline/capabilities_test.go: -------------------------------------------------------------------------------- 1 | package pktline 2 | 3 | import ( 4 | "fmt" 5 | "github.com/stretchr/testify/assert" 6 | "testing" 7 | ) 8 | 9 | const ( 10 | capabilities = "agent=spokes-pack-tests delete-refs multi_ack thin-pack no-done atomic filter=x push-cert=foo side-band side-band-64k ofs-delta shallow allow-tip-sha1-in-want allow-reachable-sha1-in-want deepen-since deepen-not deepen-relative no-progress include-tag multi_ack_detailed" 11 | ) 12 | 13 | func TestParseSimpleCapabilities(t *testing.T) { 14 | bytes := []byte(capabilities) 15 | for _, p := range []struct { 16 | capabilities []byte 17 | capability string 18 | }{ 19 | {bytes, MultiAck}, 20 | {bytes, MultiAckDetailed}, 21 | {bytes, NoDone}, 22 | {bytes, ThinPack}, 23 | {bytes, SideBand}, 24 | {bytes, SideBand64k}, 25 | {bytes, OfsDelta}, 26 | {bytes, Shallow}, 27 | {bytes, DeepenSince}, 28 | {bytes, DeepenNot}, 29 | {bytes, DeepenRelative}, 30 | {bytes, NoProgress}, 31 | {bytes, IncludeTag}, 32 | {bytes, Atomic}, 33 | {bytes, AllowTipSha1InWant}, 34 | {bytes, AllowReachableSha1InWant}, 35 | {bytes, PushCert}, 36 | {bytes, Filter}, 37 | {bytes, DeleteRefs}, 38 | {bytes, Agent}, 39 | } { 40 | t.Run( 41 | fmt.Sprintf("TestParseCapabilities(%s)", p.capabilities), 42 | func(t *testing.T) { 43 | capabilities, err := ParseCapabilities(p.capabilities) 44 | assert.NoError(t, err) 45 | cap, found := capabilities.caps[p.capability] 46 | assert.True(t, found) 47 | assert.Equal(t, cap.Name(), p.capability) 48 | }, 49 | ) 50 | } 51 | } 52 | 53 | func TestParseCapabilitiesWithArguments(t *testing.T) { 54 | bytes := []byte(capabilities) 55 | for _, p := range []struct { 56 | capabilities []byte 57 | capability string 58 | value string 59 | }{ 60 | {bytes, Agent, "spokes-pack-tests"}, 61 | {bytes, Filter, "x"}, 62 | {bytes, PushCert, "foo"}, 63 | } { 64 | t.Run( 65 | fmt.Sprintf("TestParseCapabilitiesWithArguments(%s)", p.capabilities), 66 | func(t *testing.T) { 67 | capabilities, err := ParseCapabilities(p.capabilities) 68 | assert.NoError(t, err) 69 | cap, found := capabilities.caps[p.capability] 70 | assert.True(t, found) 71 | assert.Equal(t, cap.Value(), p.value) 72 | }, 73 | ) 74 | } 75 | } 76 | 77 | func TestSafeCapabilityValue(t *testing.T) { 78 | examples := []struct { 79 | val string 80 | expected bool 81 | }{ 82 | {"", true}, 83 | {"AA:BB:CC:01", true}, 84 | {"abcdefg", true}, 85 | {"not valid", false}, 86 | {"not\tvalid", false}, 87 | {"not\rvalid", false}, 88 | {"not\nvalid", false}, 89 | } 90 | 91 | for _, ex := range examples { 92 | assert.Equal(t, ex.expected, IsSafeCapabilityValue(ex.val), "IsSafeCapabilityValue(%q)", ex.val) 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /internal/pktline/pktline.go: -------------------------------------------------------------------------------- 1 | package pktline 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "strconv" 8 | ) 9 | 10 | const ( 11 | MaxPayload = 65519 12 | HeaderSize = 4 13 | ) 14 | 15 | var FlushPktline = []byte("0000") 16 | var HeartbeatPktline = []byte("0004") 17 | 18 | type Pktline struct { 19 | buf [HeaderSize + MaxPayload + 1]byte 20 | payloadSize []byte 21 | Payload []byte 22 | CapabilitiesPayload []byte 23 | processedCapabilities bool 24 | } 25 | 26 | func New() *Pktline { 27 | pl := Pktline{} 28 | pl.Reset() 29 | return &pl 30 | } 31 | 32 | func (pl *Pktline) IsFlush() bool { 33 | return bytes.Equal(pl.payloadSize, []byte("0000")) 34 | } 35 | 36 | func (pl *Pktline) IsHeartbeat() bool { 37 | return bytes.Equal(pl.payloadSize, []byte("0004")) 38 | } 39 | 40 | func (pl *Pktline) Capabilities() (Capabilities, error) { 41 | caps, err := ParseCapabilities(pl.CapabilitiesPayload) 42 | 43 | if err != nil { 44 | return Capabilities{}, err 45 | } 46 | 47 | // Here we can implement the different rules that the different capabilities must satisfy 48 | _, sb64kFound := caps.Get(SideBand64k) 49 | _, sbFound := caps.Get(SideBand) 50 | 51 | if sb64kFound && sbFound { 52 | return Capabilities{}, fmt.Errorf("%s and %s capabilities cannot requested at the same time", SideBand64k, SideBand) 53 | } 54 | 55 | return caps, nil 56 | } 57 | 58 | // Size returns the total size of `pl` (including the length) by 59 | // parsing `pl.payloadSize`. 60 | func (pl *Pktline) Size() (int, error) { 61 | size, err := strconv.ParseUint(string(pl.payloadSize), 16, 16) 62 | if err != nil { 63 | return 0, fmt.Errorf("read-header: illformed pktline size: %w", err) 64 | } 65 | 66 | if size > HeaderSize+MaxPayload+1 { 67 | return 0, fmt.Errorf("read-header: invalid pkt-line length: %d", size) 68 | } 69 | return int(size), nil 70 | } 71 | 72 | // Reset resets the pktline to read the next pktline 73 | func (pl *Pktline) Reset() { 74 | pl.payloadSize = pl.buf[:4] 75 | pl.Payload = pl.buf[4:] 76 | } 77 | 78 | // Read reads the next pktline from `r` into `pl` (resetting `pl` 79 | // first). If `r` is already at EOF, return `io.EOF`. If EOF is 80 | // encountered after reading part but not all the pktline, return 81 | // `io.ErrUnexpectedEOF`. 82 | func (pl *Pktline) Read(r io.Reader) error { 83 | pl.Reset() 84 | // Read header 85 | if _, err := io.ReadFull(r, pl.payloadSize); err != nil { 86 | if err == io.EOF { 87 | return io.EOF 88 | } 89 | return fmt.Errorf("reading pktline size: %w", err) 90 | } 91 | 92 | size, err := pl.Size() 93 | if err != nil { 94 | return err 95 | } 96 | 97 | if size <= HeaderSize { 98 | // No payload 99 | pl.Payload = pl.buf[4:4] 100 | return nil 101 | } 102 | 103 | // Read payload 104 | if _, err := io.ReadFull(r, pl.buf[4:size]); err != nil { 105 | if err == io.EOF { 106 | err = io.ErrUnexpectedEOF 107 | } 108 | return fmt.Errorf("reading pktline data: %w", err) 109 | } 110 | 111 | pl.Payload = pl.buf[4:size] 112 | 113 | // Capabilities are (optionally) sent along the first packet line 114 | if !pl.processedCapabilities { 115 | if index := bytes.IndexByte(pl.Payload, 0); index != -1 { 116 | pl.CapabilitiesPayload = pl.Payload[index+1:] 117 | pl.processedCapabilities = true 118 | pl.Payload = pl.Payload[:index] 119 | } 120 | } 121 | 122 | return nil 123 | } 124 | -------------------------------------------------------------------------------- /internal/pktline/pktline_test.go: -------------------------------------------------------------------------------- 1 | package pktline_test 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "io" 7 | "strings" 8 | "testing" 9 | 10 | "github.com/github/spokes-receive-pack/internal/pktline" 11 | "github.com/stretchr/testify/assert" 12 | ) 13 | 14 | type expectedPktline struct { 15 | size int 16 | payload string 17 | } 18 | 19 | var expectFlush = expectedPktline{ 20 | size: 0, 21 | payload: "", 22 | } 23 | 24 | func (expected *expectedPktline) CheckEqual(pl *pktline.Pktline) error { 25 | size, err := pl.Size() 26 | if err != nil { 27 | return fmt.Errorf("invalid pktline size: %w", err) 28 | } 29 | if size != expected.size { 30 | return fmt.Errorf("incorrect pktline size: expected %d, got %d", expected.size, size) 31 | } 32 | 33 | payload := string(pl.Payload) 34 | if payload != expected.payload { 35 | return fmt.Errorf( 36 | "incorrect pktline payload: expected %q, got %q", 37 | expected.payload, payload, 38 | ) 39 | } 40 | return nil 41 | } 42 | 43 | func TestRead(t *testing.T) { 44 | for _, tc := range []struct { 45 | name string 46 | input string 47 | expected []expectedPktline 48 | }{ 49 | { 50 | name: "nothing", 51 | input: "", 52 | expected: nil, 53 | }, 54 | { 55 | name: "flush", 56 | input: "0000", 57 | expected: []expectedPktline{ 58 | expectFlush, 59 | }, 60 | }, 61 | { 62 | name: "short", 63 | input: "0002", 64 | expected: []expectedPktline{ 65 | { 66 | size: 2, 67 | payload: "", 68 | }, 69 | }, 70 | }, 71 | { 72 | name: "keepalive", 73 | input: "0004", 74 | expected: []expectedPktline{ 75 | { 76 | size: 4, 77 | payload: "", 78 | }, 79 | }, 80 | }, 81 | { 82 | name: "receive-pack-packet-line", 83 | input: "006874730d410fcb6603ace96f1dc55ea6196122532d 5a3f6be755bbb7deae50065988cbfa1ffa9ab68a refs/heads/master\n", 84 | expected: []expectedPktline{ 85 | { 86 | size: 104, 87 | payload: "74730d410fcb6603ace96f1dc55ea6196122532d 5a3f6be755bbb7deae50065988cbfa1ffa9ab68a refs/heads/master\n", 88 | }, 89 | }, 90 | }, 91 | } { 92 | t.Run(tc.name, func(t *testing.T) { 93 | pl := pktline.New() 94 | r := strings.NewReader(tc.input) 95 | for i, expected := range tc.expected { 96 | assert.NoError(t, pl.Read(r), "reading pktline") 97 | assert.NoErrorf(t, expected.CheckEqual(pl), "pktline %d incorrect", i) 98 | } 99 | 100 | err := pl.Read(r) 101 | assert.True(t, errors.Is(err, io.EOF), "expected io.EOF after reading all pktlines") 102 | }) 103 | } 104 | } 105 | 106 | func TestReadWithCapabilities(t *testing.T) { 107 | for _, tc := range []struct { 108 | name string 109 | input string 110 | expected []expectedPktline 111 | }{ 112 | { 113 | name: "single-line", 114 | input: "00820000000000000000000000000000000000000000 f9cc25952a0d66c0a388ee0decfda12a0122404d refs/heads/main\000report-status side-band-64k\n", 115 | expected: []expectedPktline{ 116 | { 117 | size: 130, 118 | payload: "0000000000000000000000000000000000000000 f9cc25952a0d66c0a388ee0decfda12a0122404d refs/heads/main", 119 | }, 120 | }, 121 | }, 122 | { 123 | name: "two-lines", 124 | input: "00860000000000000000000000000000000000000000 791d15c40c6f465afebc1ba6a11761c0b43e1c35 refs/heads/branch-1\000report-status side-band-64k\n" + 125 | "00650000000000000000000000000000000000000000 f01567bcabe2741094ab2b67155ce26a9527746f refs/heads/main", 126 | expected: []expectedPktline{ 127 | { 128 | size: 134, 129 | payload: "0000000000000000000000000000000000000000 791d15c40c6f465afebc1ba6a11761c0b43e1c35 refs/heads/branch-1", 130 | }, 131 | { 132 | size: 101, 133 | payload: "0000000000000000000000000000000000000000 f01567bcabe2741094ab2b67155ce26a9527746f refs/heads/main", 134 | }, 135 | }, 136 | }, 137 | { 138 | name: "four-lines", 139 | input: "00860000000000000000000000000000000000000000 bf7bf7a2eeee69e967d59aaab78f9022a1447b12 refs/heads/branch-1\000report-status side-band-64k\n" + 140 | "00690000000000000000000000000000000000000000 f13ce6e8f50b0aa7aae764434ee15a414da3f50f refs/heads/branch-2" + 141 | "00690000000000000000000000000000000000000000 6d0be418a4c1776981726d1a8d39cd7f790efb61 refs/heads/branch-3" + 142 | "00650000000000000000000000000000000000000000 5bf437d78e72522939e6b17aeed1a5b0ae73a100 refs/heads/main", 143 | expected: []expectedPktline{ 144 | { 145 | size: 134, 146 | payload: "0000000000000000000000000000000000000000 bf7bf7a2eeee69e967d59aaab78f9022a1447b12 refs/heads/branch-1", 147 | }, 148 | { 149 | size: 105, 150 | payload: "0000000000000000000000000000000000000000 f13ce6e8f50b0aa7aae764434ee15a414da3f50f refs/heads/branch-2", 151 | }, 152 | { 153 | size: 105, 154 | payload: "0000000000000000000000000000000000000000 6d0be418a4c1776981726d1a8d39cd7f790efb61 refs/heads/branch-3", 155 | }, 156 | { 157 | size: 101, 158 | payload: "0000000000000000000000000000000000000000 5bf437d78e72522939e6b17aeed1a5b0ae73a100 refs/heads/main", 159 | }, 160 | }, 161 | }, 162 | } { 163 | t.Run(tc.name, func(t *testing.T) { 164 | pl := pktline.New() 165 | r := strings.NewReader(tc.input) 166 | for i, expected := range tc.expected { 167 | assert.NoError(t, pl.Read(r), "reading pktline") 168 | assert.NoErrorf(t, expected.CheckEqual(pl), "pktline %d incorrect", i) 169 | } 170 | 171 | err := pl.Read(r) 172 | assert.True(t, errors.Is(err, io.EOF), "expected io.EOF after reading all pktlines") 173 | 174 | caps, err := pl.Capabilities() 175 | assert.NoError(t, err) 176 | 177 | assert.Equal(t, "report-status", caps.ReportStatus().Name()) 178 | assert.Equal(t, "side-band-64k", caps.SideBand64k().Name()) 179 | }) 180 | } 181 | } 182 | 183 | func TestReadErrors(t *testing.T) { 184 | for _, tc := range []struct { 185 | name string 186 | input string 187 | expectedError string 188 | }{ 189 | { 190 | name: "truncated-size", 191 | input: "01", 192 | expectedError: "unexpected EOF", 193 | }, 194 | { 195 | name: "invalid-size", 196 | input: "foob", 197 | expectedError: "illformed pktline size", 198 | }, 199 | { 200 | name: "truncated-payload", 201 | input: "fff4" + "2" + "not enough bytes", 202 | expectedError: "unexpected EOF", 203 | }, 204 | { 205 | name: "size-too-large", 206 | input: "fff5" + "2" + "these bytes not read", 207 | expectedError: "read-header: invalid pkt-line length", 208 | }, 209 | } { 210 | t.Run(tc.name, func(t *testing.T) { 211 | pl := pktline.New() 212 | r := strings.NewReader(tc.input) 213 | err := pl.Read(r) 214 | if !strings.Contains(err.Error(), tc.expectedError) { 215 | t.Fatal( 216 | "expected error '"+tc.expectedError+ 217 | "' after reading all pktlines; got ", err, 218 | ) 219 | } 220 | }) 221 | } 222 | } 223 | -------------------------------------------------------------------------------- /internal/receivepack/receivepack.go: -------------------------------------------------------------------------------- 1 | package receivepack 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io" 7 | "os/exec" 8 | ) 9 | 10 | // ReceivePack is used to model a receive-pack executor 11 | type ReceivePack struct { 12 | stdin io.Reader 13 | stdout io.Writer 14 | stderr io.Writer 15 | args []string 16 | } 17 | 18 | // NewReceivePack returns a pointer to a ReceivePack executor 19 | func NewReceivePack(stdin io.Reader, stdout, stderr io.Writer, args []string) *ReceivePack { 20 | return &ReceivePack{ 21 | stdin: stdin, 22 | stdout: stdout, 23 | stderr: stderr, 24 | args: args, 25 | } 26 | } 27 | 28 | // Execute executes the git-receive-pack program spawning the actual Git process 29 | func (r *ReceivePack) Execute(ctx context.Context) error { 30 | cmd := exec.CommandContext(ctx, "git-receive-pack", r.args...) 31 | cmd.Stdin = r.stdin 32 | cmd.Stdout = r.stdout 33 | cmd.Stderr = r.stderr 34 | 35 | if err := cmd.Run(); err != nil { 36 | return fmt.Errorf("unexpected error executing the git-receive-pack Git command: %w", err) 37 | } 38 | 39 | return nil 40 | } 41 | -------------------------------------------------------------------------------- /internal/sockstat/sockstat.go: -------------------------------------------------------------------------------- 1 | package sockstat 2 | 3 | import ( 4 | "os" 5 | "strconv" 6 | "strings" 7 | ) 8 | 9 | // Prefix is the prefix that all sockstat environment variable names must have. 10 | const Prefix = "GIT_SOCKSTAT_VAR_" 11 | 12 | // GetString looks up the given sockstat var name in the environment and 13 | // interprets it as a string. If the var isn't present, the empty string is 14 | // returned. 15 | func GetString(name string) string { 16 | return StringValue(os.Getenv(Prefix + name)) 17 | } 18 | 19 | // GetUint32 looks up the given sockstat var name in the environment and 20 | // interprets it as a uint32. If the var isn't present or isn't a uint32, 0 is 21 | // returned. 22 | func GetUint32(name string) uint32 { 23 | return Uint32Value(os.Getenv(Prefix + name)) 24 | } 25 | 26 | // GetBool looks up the given sockstat var name in the environment and 27 | // interprets it as a bool. If the var isn't present or isn't true, false is 28 | // returned. 29 | func GetBool(name string) bool { 30 | return BoolValue(os.Getenv(Prefix + name)) 31 | } 32 | 33 | // StringValue returns the string version of the given sockstat var. For the 34 | // most part, this means just returning the given string. However, if the input 35 | // has a uint or bool prefix, strip that off so that it looks like we parsed 36 | // the value and then stringified it. 37 | func StringValue(s string) string { 38 | parts := strings.SplitN(s, ":", 2) 39 | if len(parts) == 2 && (parts[0] == "uint" || parts[0] == "bool") { 40 | return parts[1] 41 | } 42 | return s 43 | } 44 | 45 | // Uint32Value parses a string like "uint32:123" and returns the parsed uint32 46 | // like 123. If the prefix is missing or the value isn't a uint32, return 0. 47 | func Uint32Value(s string) uint32 { 48 | s, ok := strings.CutPrefix(s, "uint:") 49 | if !ok { 50 | return 0 51 | } 52 | val, err := strconv.ParseUint(s, 10, 32) 53 | if err != nil { 54 | return 0 55 | } 56 | return uint32(val) 57 | } 58 | 59 | // BoolValue interprets "bool:true" as true and anything else as false. 60 | func BoolValue(s string) bool { 61 | return s == "bool:true" 62 | } 63 | -------------------------------------------------------------------------------- /internal/sockstat/sockstat_test.go: -------------------------------------------------------------------------------- 1 | package sockstat 2 | 3 | import "testing" 4 | 5 | func TestUint32(t *testing.T) { 6 | examples := []struct { 7 | input string 8 | output uint32 9 | }{ 10 | {"", 0}, 11 | {"123", 0}, 12 | {"abc", 0}, 13 | {"bool:true", 0}, 14 | {"bool:false", 0}, 15 | {"uint:-1", 0}, 16 | {"uint:1", 1}, 17 | {"uint:4294967295", 4294967295}, 18 | {"uint:4294967296", 0}, 19 | {"uint:4294967297", 0}, 20 | {"uint:abc", 0}, 21 | {"uint: 1", 0}, 22 | {"uint:1 ", 0}, 23 | } 24 | 25 | for _, ex := range examples { 26 | actual := Uint32Value(ex.input) 27 | if actual != ex.output { 28 | t.Errorf("Uint32Value(%q): expected %d, but was %d", ex.input, ex.output, actual) 29 | } 30 | } 31 | } 32 | 33 | func TestString(t *testing.T) { 34 | examples := []struct { 35 | input string 36 | output string 37 | }{ 38 | {"", ""}, 39 | {"123", "123"}, 40 | {"abc", "abc"}, 41 | {"bool:true", "true"}, 42 | {"bool:false", "false"}, 43 | {"uint:-1", "-1"}, 44 | {"uint:1", "1"}, 45 | {"uint:4294967295", "4294967295"}, 46 | {"uint:4294967296", "4294967296"}, 47 | {"bool:uint:anything", "uint:anything"}, 48 | {"uint:bool:anything", "bool:anything"}, 49 | {"anything:uint:bool", "anything:uint:bool"}, 50 | } 51 | 52 | for _, ex := range examples { 53 | actual := StringValue(ex.input) 54 | if actual != ex.output { 55 | t.Errorf("StringValue(%q): expected %q, but was %q", ex.input, ex.output, actual) 56 | } 57 | } 58 | } 59 | 60 | func TestBool(t *testing.T) { 61 | examples := []struct { 62 | input string 63 | output bool 64 | }{ 65 | // These are the only well-formed bool values. 66 | {"bool:true", true}, 67 | {"bool:false", false}, 68 | // All other values end up as false. 69 | {"", false}, 70 | {"123", false}, 71 | {"abc", false}, 72 | {"uint:-1", false}, 73 | {"uint:0", false}, 74 | {"uint:1", false}, 75 | {"uint:4294967295", false}, 76 | {"uint:4294967296", false}, 77 | {"bool:uint:anything", false}, 78 | {"uint:bool:anything", false}, 79 | {"anything:uint:bool", false}, 80 | } 81 | 82 | for _, ex := range examples { 83 | actual := BoolValue(ex.input) 84 | if actual != ex.output { 85 | t.Errorf("BoolValue(%q): expected %v, but was %v", ex.input, ex.output, actual) 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /internal/spokes/testdata/lots-of-refs.git/HEAD: -------------------------------------------------------------------------------- 1 | ref: refs/heads/main 2 | -------------------------------------------------------------------------------- /internal/spokes/testdata/lots-of-refs.git/config: -------------------------------------------------------------------------------- 1 | [core] 2 | repositoryformatversion = 0 3 | filemode = true 4 | bare = true 5 | ignorecase = true 6 | precomposeunicode = true 7 | -------------------------------------------------------------------------------- /internal/spokes/testdata/lots-of-refs.git/description: -------------------------------------------------------------------------------- 1 | Unnamed repository; edit this file 'description' to name the repository. 2 | -------------------------------------------------------------------------------- /internal/spokes/testdata/lots-of-refs.git/info/exclude: -------------------------------------------------------------------------------- 1 | # git ls-files --others --exclude-from=.git/info/exclude 2 | # Lines that start with '#' are comments. 3 | # For a project mostly in C, the following would be a good set of 4 | # exclude patterns (uncomment them if you want to use them): 5 | # *.[oa] 6 | # *~ 7 | -------------------------------------------------------------------------------- /internal/spokes/testdata/lots-of-refs.git/info/refs: -------------------------------------------------------------------------------- 1 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/heads/main 2 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-1 3 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-10 4 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-100 5 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-11 6 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-12 7 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-13 8 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-14 9 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-15 10 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-16 11 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-17 12 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-18 13 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-19 14 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-2 15 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-20 16 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-21 17 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-22 18 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-23 19 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-24 20 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-25 21 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-26 22 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-27 23 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-28 24 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-29 25 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-3 26 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-30 27 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-31 28 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-32 29 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-33 30 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-34 31 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-35 32 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-36 33 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-37 34 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-38 35 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-39 36 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-4 37 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-40 38 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-41 39 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-42 40 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-43 41 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-44 42 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-45 43 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-46 44 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-47 45 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-48 46 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-49 47 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-5 48 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-50 49 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-51 50 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-52 51 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-53 52 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-54 53 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-55 54 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-56 55 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-57 56 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-58 57 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-59 58 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-6 59 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-60 60 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-61 61 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-62 62 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-63 63 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-64 64 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-65 65 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-66 66 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-67 67 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-68 68 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-69 69 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-7 70 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-70 71 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-71 72 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-72 73 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-73 74 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-74 75 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-75 76 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-76 77 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-77 78 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-78 79 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-79 80 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-8 81 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-80 82 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-81 83 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-82 84 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-83 85 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-84 86 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-85 87 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-86 88 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-87 89 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-88 90 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-89 91 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-9 92 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-90 93 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-91 94 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-92 95 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-93 96 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-94 97 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-95 98 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-96 99 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-97 100 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-98 101 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-99 102 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-1 103 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-10 104 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-100 105 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-11 106 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-12 107 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-13 108 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-14 109 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-15 110 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-16 111 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-17 112 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-18 113 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-19 114 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-2 115 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-20 116 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-21 117 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-22 118 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-23 119 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-24 120 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-25 121 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-26 122 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-27 123 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-28 124 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-29 125 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-3 126 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-30 127 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-31 128 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-32 129 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-33 130 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-34 131 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-35 132 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-36 133 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-37 134 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-38 135 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-39 136 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-4 137 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-40 138 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-41 139 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-42 140 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-43 141 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-44 142 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-45 143 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-46 144 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-47 145 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-48 146 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-49 147 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-5 148 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-50 149 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-51 150 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-52 151 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-53 152 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-54 153 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-55 154 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-56 155 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-57 156 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-58 157 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-59 158 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-6 159 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-60 160 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-61 161 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-62 162 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-63 163 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-64 164 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-65 165 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-66 166 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-67 167 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-68 168 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-69 169 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-7 170 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-70 171 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-71 172 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-72 173 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-73 174 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-74 175 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-75 176 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-76 177 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-77 178 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-78 179 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-79 180 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-8 181 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-80 182 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-81 183 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-82 184 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-83 185 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-84 186 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-85 187 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-86 188 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-87 189 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-88 190 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-89 191 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-9 192 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-90 193 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-91 194 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-92 195 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-93 196 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-94 197 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-95 198 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-96 199 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-97 200 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-98 201 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-99 202 | -------------------------------------------------------------------------------- /internal/spokes/testdata/lots-of-refs.git/objects/info/packs: -------------------------------------------------------------------------------- 1 | P pack-714209910910d57afd6c8c83dcce4057d4f10c0b.pack 2 | 3 | -------------------------------------------------------------------------------- /internal/spokes/testdata/lots-of-refs.git/objects/pack/pack-714209910910d57afd6c8c83dcce4057d4f10c0b.bitmap: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/spokes-receive-pack/acac8763c60f636c44baaf5c3887895cf5f55c30/internal/spokes/testdata/lots-of-refs.git/objects/pack/pack-714209910910d57afd6c8c83dcce4057d4f10c0b.bitmap -------------------------------------------------------------------------------- /internal/spokes/testdata/lots-of-refs.git/objects/pack/pack-714209910910d57afd6c8c83dcce4057d4f10c0b.idx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/spokes-receive-pack/acac8763c60f636c44baaf5c3887895cf5f55c30/internal/spokes/testdata/lots-of-refs.git/objects/pack/pack-714209910910d57afd6c8c83dcce4057d4f10c0b.idx -------------------------------------------------------------------------------- /internal/spokes/testdata/lots-of-refs.git/objects/pack/pack-714209910910d57afd6c8c83dcce4057d4f10c0b.pack: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/spokes-receive-pack/acac8763c60f636c44baaf5c3887895cf5f55c30/internal/spokes/testdata/lots-of-refs.git/objects/pack/pack-714209910910d57afd6c8c83dcce4057d4f10c0b.pack -------------------------------------------------------------------------------- /internal/spokes/testdata/lots-of-refs.git/packed-refs: -------------------------------------------------------------------------------- 1 | # pack-refs with: peeled fully-peeled sorted 2 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-1 3 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-10 4 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-100 5 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-11 6 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-12 7 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-13 8 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-14 9 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-15 10 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-16 11 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-17 12 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-18 13 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-19 14 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-2 15 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-20 16 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-21 17 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-22 18 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-23 19 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-24 20 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-25 21 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-26 22 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-27 23 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-28 24 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-29 25 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-3 26 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-30 27 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-31 28 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-32 29 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-33 30 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-34 31 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-35 32 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-36 33 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-37 34 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-38 35 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-39 36 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-4 37 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-40 38 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-41 39 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-42 40 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-43 41 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-44 42 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-45 43 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-46 44 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-47 45 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-48 46 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-49 47 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-5 48 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-50 49 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-51 50 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-52 51 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-53 52 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-54 53 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-55 54 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-56 55 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-57 56 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-58 57 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-59 58 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-6 59 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-60 60 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-61 61 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-62 62 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-63 63 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-64 64 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-65 65 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-66 66 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-67 67 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-68 68 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-69 69 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-7 70 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-70 71 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-71 72 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-72 73 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-73 74 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-74 75 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-75 76 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-76 77 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-77 78 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-78 79 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-79 80 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-8 81 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-80 82 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-81 83 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-82 84 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-83 85 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-84 86 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-85 87 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-86 88 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-87 89 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-88 90 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-89 91 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-9 92 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-90 93 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-91 94 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-92 95 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-93 96 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-94 97 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-95 98 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-96 99 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-97 100 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-98 101 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-99 102 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-1 103 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-10 104 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-100 105 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-11 106 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-12 107 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-13 108 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-14 109 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-15 110 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-16 111 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-17 112 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-18 113 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-19 114 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-2 115 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-20 116 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-21 117 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-22 118 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-23 119 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-24 120 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-25 121 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-26 122 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-27 123 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-28 124 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-29 125 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-3 126 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-30 127 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-31 128 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-32 129 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-33 130 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-34 131 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-35 132 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-36 133 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-37 134 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-38 135 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-39 136 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-4 137 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-40 138 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-41 139 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-42 140 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-43 141 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-44 142 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-45 143 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-46 144 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-47 145 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-48 146 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-49 147 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-5 148 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-50 149 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-51 150 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-52 151 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-53 152 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-54 153 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-55 154 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-56 155 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-57 156 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-58 157 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-59 158 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-6 159 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-60 160 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-61 161 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-62 162 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-63 163 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-64 164 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-65 165 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-66 166 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-67 167 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-68 168 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-69 169 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-7 170 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-70 171 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-71 172 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-72 173 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-73 174 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-74 175 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-75 176 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-76 177 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-77 178 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-78 179 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-79 180 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-8 181 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-80 182 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-81 183 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-82 184 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-83 185 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-84 186 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-85 187 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-86 188 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-87 189 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-88 190 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-89 191 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-9 192 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-90 193 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-91 194 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-92 195 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-93 196 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-94 197 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-95 198 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-96 199 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-97 200 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-98 201 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 refs/tags/tag-aaaa-lakdjsf-asdfjkasdklfj-asdkfj-99 202 | -------------------------------------------------------------------------------- /internal/spokes/testdata/lots-of-refs.git/refs/heads/main: -------------------------------------------------------------------------------- 1 | 6a9ee41101de417acd4db5b7a18b66a5e1b54496 2 | -------------------------------------------------------------------------------- /ownership.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 1 3 | ownership: 4 | - name: spokes-receive-pack 5 | long_name: spokes-receive-pack 6 | description: spokes-receive-pack is a replacement for git-receive-pack containing 7 | only the functionality GitHub needs to process an incoming pack. 8 | kind: code 9 | repo: https://github.com/github/spokes-receive-pack 10 | qos: experimental 11 | team: github/git-systems 12 | team_slack: git-systems 13 | exec_sponsor: shayneburgess 14 | product_manager: andrewakim 15 | dependencies: 16 | - spokesd 17 | sev1: 18 | pagerduty: https://github.pagerduty.com/escalation_policies#PTID6HO 19 | tta: 20m 20 | sev2: 21 | issue: https://github.com/github/git-systems-alerts/issues 22 | tta: 1 business day 23 | sev3: 24 | slack: git-systems-alerts 25 | tta: 3 business days 26 | tier: 1 27 | -------------------------------------------------------------------------------- /spokes-receive-pack.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io" 7 | "os" 8 | 9 | "github.com/github/spokes-receive-pack/internal/receivepack" 10 | "github.com/github/spokes-receive-pack/internal/sockstat" 11 | "github.com/github/spokes-receive-pack/internal/spokes" 12 | ) 13 | 14 | var BuildVersion string 15 | 16 | func main() { 17 | exitCode, err := mainImpl(os.Stdin, os.Stdout, os.Stderr, os.Args[1:]) 18 | if err != nil { 19 | fmt.Fprintf(os.Stderr, "error: %v\n", err) 20 | } 21 | os.Exit(exitCode) 22 | } 23 | 24 | func mainImpl(stdin io.Reader, stdout, stderr io.Writer, args []string) (int, error) { 25 | ctx := context.Background() 26 | if !sockstat.GetBool("spokes_quarantine") { 27 | rp := receivepack.NewReceivePack(stdin, stdout, stderr, args) 28 | if err := rp.Execute(ctx); err != nil { 29 | return 1, fmt.Errorf("unexpected error running receive pack: %w", err) 30 | } 31 | 32 | return 0, nil 33 | } 34 | 35 | return spokes.Exec(ctx, stdin, stdout, stderr, args, BuildVersion) 36 | } 37 | --------------------------------------------------------------------------------