├── .gitignore ├── rustfmt.toml ├── Makefile ├── .github └── workflows │ └── ci.yaml ├── CONTRIBUTING.md ├── README.md ├── Cargo.toml ├── LICENSE └── src └── main.rs /.gitignore: -------------------------------------------------------------------------------- 1 | # Copyright 2020 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | Cargo.lock 16 | /target 17 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | # Copyright 2025 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | use_small_heuristics = "Max" 16 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Copyright 2025 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | .PHONY: test 16 | test: 17 | RUSTFLAGS="-D warnings" cargo build --all-targets --release 18 | RUSTFLAGS="-D warnings" cargo clippy --all-targets 19 | cargo fmt --check 20 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2020 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | name: ci 16 | on: pull_request 17 | 18 | jobs: 19 | build: 20 | runs-on: ubuntu-latest 21 | 22 | steps: 23 | - name: Clone repository 24 | uses: actions/checkout@v4 25 | 26 | - name: Test 27 | run: make test 28 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to Contribute 2 | 3 | We'd love to accept your patches and contributions to this project. There are 4 | just a few small guidelines you need to follow. 5 | 6 | ## Contributor License Agreement 7 | 8 | Contributions to this project must be accompanied by a Contributor License 9 | Agreement. You (or your employer) retain the copyright to your contribution; 10 | this simply gives us permission to use and redistribute your contributions as 11 | part of the project. Head over to to see 12 | your current agreements on file or to sign a new one. 13 | 14 | You generally only need to submit a CLA once, so if you've already submitted one 15 | (even if it was for a different project), you probably don't need to do it 16 | again. 17 | 18 | ## Code reviews 19 | 20 | All submissions, including submissions by project members, require review. We 21 | use GitHub pull requests for this purpose. Consult 22 | [GitHub Help](https://help.github.com/articles/about-pull-requests/) for more 23 | information on using pull requests. 24 | 25 | ## Community Guidelines 26 | 27 | This project follows [Google's Open Source Community 28 | Guidelines](https://opensource.google/conduct/). 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # git-tree 2 | 3 | *This is not an officially supported Google product.* 4 | 5 | `git-tree` is a wrapper around `git log` that heuristically determines what set 6 | of commits should be displayed. It is designed for use with branch-heavy 7 | workflows similar to those supported by the [Mercurial `evolve` 8 | extension](https://www.mercurial-scm.org/wiki/EvolveExtension). 9 | 10 | Command-line arguments are passed through to `git log`, allowing the user to set 11 | up their own formatting options. 12 | 13 | For example, I have the following alias in my `.bashrc` to invoke `git-tree`: 14 | 15 | ``` 16 | alias git-tree='git-tree --format="%C(auto)%h %d %<(50,trunc)%s" --graph' 17 | ``` 18 | 19 | This produces output similar to the following (albeit colorized by default, 20 | which makes it much more readable than this example): 21 | ``` 22 | * 49aaffb (origin/step7-last-futures, step7-last-futures) Remove the remaining uses of futures from.. 23 | * 4450f7b (HEAD -> step6-buttons-2, origin/step6-buttons-2) Remove another use of button futures. 24 | * f60a7dc Add a dynamic call mechanism for client callbacks. 25 | | * f2c8713 (lw2) More scratch work for virtualized time. 26 | | | * 6e3e0c3 (origin/virtclk-scratch, virtclk-scratch) Work on virtualized lightweight clock. 27 | | |/ 28 | |/| 29 | * | 34282e5 (origin/step1-rng-size, step1-rng-size) Migrate the crypto library to the lightweight RN.. 30 | * | 5959c85 (origin/futures-size, futures-size) Add the size dump for the futures-based OpenSK! 31 | * | 7f294f9 Start working on a lightweight RNG driver. 32 | |/ 33 | * bdb3b8c (origin/original-size, original-size) Store information about the size of the app with.. 34 | * e804c89 (origin/submods-to-dirs, submods-to-dirs) Replace the submodules with local directories. T.. 35 | * 57e79c1 (origin/master, origin/HEAD, master) Merge pull request #82 from jmichelp/master 36 | ``` 37 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | # Copyright 2024 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | [package] 16 | name = "git-tree" 17 | authors = ["Johnathan Van Why "] 18 | categories = ["command-line-utilities", "development-tools"] 19 | description = """Tool that invokes git log --graph, automatically determining \ 20 | relevant commits to include.""" 21 | edition = "2021" 22 | keywords = ["git"] 23 | license = "Apache-2.0" 24 | repository = "https://github.com/google/git-tree" 25 | version = "0.1.0" 26 | 27 | [lints.clippy] 28 | all = { level = "deny", priority = -1 } 29 | allow_attributes = "allow" 30 | arbitrary_source_item_ordering = "allow" 31 | blanket_clippy_restriction_lints = "allow" 32 | cargo = { level = "deny", priority = -1 } 33 | complexity = { level = "deny", priority = -1 } 34 | correctness = { level = "deny", priority = -1 } 35 | else_if_without_else = "allow" 36 | expect_used = "allow" 37 | get_unwrap = "allow" 38 | implicit_return = "allow" 39 | iter_over_hash_type = "allow" 40 | iter_with_drain = "allow" 41 | min_ident_chars = "allow" 42 | missing_docs_in_private_items = "allow" 43 | nursery = { level = "deny", priority = -1 } 44 | pedantic = { level = "deny", priority = -1 } 45 | perf = { level = "deny", priority = -1 } 46 | restriction = { level = "deny", priority = -1 } 47 | shadow_reuse = "allow" 48 | single_call_fn = "allow" 49 | style = { level = "deny", priority = -1 } 50 | suspicious = { level = "deny", priority = -1 } 51 | unwrap_used = "allow" 52 | 53 | [profile.release] 54 | codegen-units = 1 55 | lto = true 56 | panic = "abort" 57 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | //! A wrapper around `git log` that heuristically determines what set of commits 16 | //! should be displayed. 17 | 18 | // The "interesting branches" are all local branches and all remote branches 19 | // that are tracked by a local branch. The "interesting commits" are the commits 20 | // pointed to by the interesting branches plus the HEAD commit. This tool 21 | // displays the interesting commits, their collective merge bases, and any 22 | // commits on the paths between the merge bases and the interesting commits. 23 | 24 | use core::iter::{once, repeat_n}; 25 | use core::str; 26 | use std::collections::{HashMap, HashSet}; 27 | use std::env::args_os; 28 | use std::io::{BufRead as _, BufReader}; 29 | use std::process::{Command, Stdio}; 30 | 31 | /// Returns all interesting branches. Note that some commits may be in the list 32 | /// multiple times under different names. 33 | /// Precondition: `buffer` must be empty 34 | /// Postcondition: `buffer` will be empty 35 | fn interesting_branches(buffer: &mut Vec) -> Vec { 36 | // This considers a branch interesting if it is a local branch or if it has 37 | // the same name as a local branch. 38 | let mut git = Command::new("git") 39 | .args(["branch", "-a", "--format=%(refname)"]) 40 | .stdout(Stdio::piped()) 41 | .spawn() 42 | .expect("failed to run git"); 43 | let mut locals = HashSet::new(); 44 | let mut remotes = vec![]; 45 | let mut reader = BufReader::new(git.stdout.as_mut().unwrap()); 46 | while let Some(len) = 47 | reader.read_until(b'\n', buffer).expect("git stdout read failed").checked_sub(1) 48 | { 49 | if buffer.first_chunk() == Some(b"refs/remotes/") { 50 | remotes.push(buffer.get(b"refs/remotes/".len()..len).unwrap().to_vec()); 51 | } else if buffer.first_chunk() == Some(b"refs/heads/") { 52 | locals.insert(buffer.get(b"refs/heads/".len()..len).unwrap().into()); 53 | } 54 | buffer.clear(); 55 | } 56 | drop(reader); 57 | let mut interesting = vec![]; 58 | for remote in remotes { 59 | let Some(idx) = remote.iter().position(|&b| b == b'/') else { continue }; 60 | #[allow(clippy::arithmetic_side_effects, reason = "idx is less than buffer.len()")] 61 | let (_, name) = remote.split_at(idx + 1); 62 | if locals.contains(name) { 63 | interesting.push(String::from_utf8(remote).expect("non-utf-8 branch")); 64 | } 65 | } 66 | interesting.extend( 67 | locals.into_iter().map(|local| String::from_utf8(local).expect("non-utf-8 branch")), 68 | ); 69 | let status = git.wait().expect("failed to wait for git"); 70 | assert!(status.success(), "git returned unsuccessful status {status}"); 71 | interesting 72 | } 73 | 74 | /// Returns all merge bases of the interesting commits. 75 | /// Precondition: `buffer` must be empty 76 | /// Postcondition: `buffer` will be empty 77 | fn merge_bases(buffer: &mut Vec, interesting_branches: &Vec) -> Vec { 78 | let mut git = Command::new("git") 79 | .args(["merge-base", "-a", "--octopus", "HEAD"]) 80 | .args(interesting_branches) 81 | .stdout(Stdio::piped()) 82 | .spawn() 83 | .expect("failed to run git"); 84 | let mut merge_bases = Vec::with_capacity(1); 85 | let mut reader = BufReader::new(git.stdout.as_mut().unwrap()); 86 | while let Some(len) = 87 | reader.read_until(b'\n', buffer).expect("git stdout read failed").checked_sub(1) 88 | { 89 | // Reserve enough space for the merge base plus a trailing ^@ (used in 90 | // the final `git log` invocation). 91 | #[allow( 92 | clippy::arithmetic_side_effects, 93 | reason = "len is < the size of an allocation so adding 2 shouldn't overflow usize" 94 | )] 95 | let mut merge_base = String::with_capacity(len + 2); 96 | merge_base 97 | .push_str(str::from_utf8(buffer.get(..len).unwrap()).expect("non-utf-8 git output")); 98 | merge_bases.push(merge_base); 99 | buffer.clear(); 100 | } 101 | drop(reader); 102 | let status = git.wait().expect("failed to wait for git"); 103 | assert!(status.success(), "git returned unsuccessful status {status}"); 104 | merge_bases 105 | } 106 | 107 | /// Computes the include and exclude lists to pass to git. The first list 108 | /// returned is the inclusion list, the second is the exclusion list. 109 | /// Precondition: buffer is empty. 110 | fn includes_excludes( 111 | mut buffer: Vec, 112 | interesting_branches: Vec, 113 | merge_bases: &Vec, 114 | ) -> (Vec, Vec) { 115 | // We want to show the interesting commits, merge bases, and the commits on 116 | // a path between the two. That is equivalent to showing all commits which 117 | // satisfy: 118 | // 1. The commit is reachable from an interesting commit, and 119 | // 2. A merge base is reachable from the commit. 120 | // This graph traversal computes the include and exclude arguments to pass 121 | // to git log to show the above set of commits. 122 | // We ask `git rev-list` to print all commits that are reachable from an 123 | // interesting commit and not reachable from a merge base (note: this 124 | // excludes the merge bases themselves). Every commit that git returns 125 | // satisfies condition 1, but not all satisfy condition 2 (it may return 126 | // commits that cannot reach a merge base). 127 | // Since all such commits satisfy condition 1, we only really have to look 128 | // at condition 2. If a commit can reach a merge base, then it should be 129 | // shown, and we call it "visible". To easily compute which commits are 130 | // visible, we ask git rev-list to print out the commits in reverse 131 | // topological order, so that we visit all a commit's parents before we 132 | // visit that commit. That way, when we visit a node, we know it is visible 133 | // iff it has a visible parent. 134 | // Once the graph traversal is complete: 135 | // A) The includes list should consist of every childless visible commit. 136 | // B) The excludes list should consist of every invisible commit that does 137 | // not have an invisible child. 138 | // Fortunately, we can track whether a node has a (visible?) child as we 139 | // traverse the graph. When we first add a commit, we mark it as having no 140 | // (visible?) child, then we update that if we encounter its children. Note 141 | // that we do not need to track invisible nodes that have invisible children 142 | // -- they can be forgotten about entirely once detected. 143 | 144 | #[derive(Clone, Copy, PartialEq)] 145 | enum NodeState { 146 | // This node should not be visible in the final graph (it does not see a 147 | // merge base), and we have not yet explored any invisible child commits 148 | // of it. Note that InvisibleParent does not exist because if we find an 149 | // invisible child node of an InvisibleChild node, we remove the 150 | // InvisibleChild node entirely. 151 | InvisibleChild, 152 | 153 | // This node should be visible in the final graph (it does see a merge 154 | // base), and we've found a child node of it. 155 | VisibleParent, 156 | 157 | // This node should be visible in the final graph, and we have not yet 158 | // explored a child node of it. 159 | VisibleChild, 160 | } 161 | impl NodeState { 162 | /// Returns whether this is a visible node. 163 | fn is_visible(self) -> bool { 164 | self != Self::InvisibleChild 165 | } 166 | } 167 | 168 | let mut git = Command::new("git") 169 | .args(["rev-list", "--parents", "--reverse", "--topo-order", "HEAD"]) 170 | .args(interesting_branches) 171 | .arg("--not") 172 | .args(merge_bases) 173 | .stdout(Stdio::piped()) 174 | .spawn() 175 | .expect("failed to run git"); 176 | let mut nodes: Vec<_> = repeat_n(NodeState::VisibleChild, merge_bases.len()).collect(); 177 | let mut free_slots = Vec::with_capacity(2); 178 | let mut node_lookup: HashMap<_, _> = 179 | merge_bases.iter().enumerate().map(|(i, id)| (id.clone().into(), i)).collect(); 180 | // (index range of the parent's id in buffer, Option) for 181 | // each parent of this commit. 182 | let mut parents = Vec::with_capacity(2); 183 | let mut reader = BufReader::new(git.stdout.as_mut().unwrap()); 184 | while let Some(len) = 185 | reader.read_until(b'\n', &mut buffer).expect("git stdout read failed").checked_sub(1) 186 | { 187 | // Construct an iterator over the indexes of the returned commit IDs. 188 | // The first ID is the ID of this commit, the rest are this commit's 189 | // parents. 190 | let mut next_start = 0; // Start of the next range. 191 | #[allow(clippy::arithmetic_side_effects, reason = "i is at most buffer.len()")] 192 | let mut id_ranges = buffer 193 | // Iterate over the buffer excluding the newline. 194 | .get(..len) 195 | .unwrap() 196 | .iter() 197 | // enumerate-filter-map to get the indexes of the spaces 198 | .enumerate() 199 | .filter(|&(_, &b)| b == b' ') 200 | .map(|(i, _)| i) 201 | // End with the index of the ending newline 202 | .chain(once(len)) 203 | .map(|i| { 204 | let start = next_start; 205 | next_start = i + 1; // + 1 skips the space 206 | start..i 207 | }); 208 | // This commit's ID. 209 | let id = buffer.get(id_ranges.next().expect("empty rev-list output line")).unwrap(); 210 | parents 211 | .extend(id_ranges.map(|range| { 212 | (range.clone(), node_lookup.get(buffer.get(range).unwrap()).copied()) 213 | })); 214 | let visible = parents 215 | .iter() 216 | .filter_map(|&(_, idx)| idx) 217 | .any(|idx| nodes.get(idx).unwrap().is_visible()); 218 | let new_state = if visible { 219 | for idx in parents.drain(..).filter_map(|(_, idx)| idx) { 220 | let parent = nodes.get_mut(idx).unwrap(); 221 | if *parent == NodeState::VisibleChild { 222 | *parent = NodeState::VisibleParent; 223 | } 224 | } 225 | NodeState::VisibleChild 226 | } else { 227 | for (range, parent_idx) in parents.drain(..) { 228 | let Some(parent_idx) = parent_idx else { continue }; 229 | if nodes.get(parent_idx) != Some(&NodeState::InvisibleChild) { 230 | continue; 231 | } 232 | node_lookup.remove(buffer.get(range).unwrap()); 233 | free_slots.push(parent_idx); 234 | } 235 | NodeState::InvisibleChild 236 | }; 237 | if let Some(new_idx) = free_slots.pop() { 238 | node_lookup.insert(id.to_vec(), new_idx); 239 | *nodes.get_mut(new_idx).unwrap() = new_state; 240 | } else { 241 | node_lookup.insert(id.to_vec(), nodes.len()); 242 | nodes.push(new_state); 243 | } 244 | buffer.clear(); 245 | } 246 | drop(reader); 247 | drop(parents); 248 | drop(free_slots); 249 | drop(buffer); 250 | let mut includes = vec![]; 251 | let mut excludes = vec![]; 252 | for (id, idx) in node_lookup { 253 | match *nodes.get(idx).unwrap() { 254 | NodeState::InvisibleChild => { 255 | excludes.push(String::from_utf8(id).expect("non-utf-8 id")); 256 | } 257 | NodeState::VisibleChild => includes.push(String::from_utf8(id).expect("non-utf-8 id")), 258 | NodeState::VisibleParent => {} 259 | } 260 | } 261 | drop(nodes); 262 | let status = git.wait().expect("failed to wait for git"); 263 | assert!(status.success(), "git returned unsuccessful status {status}"); 264 | (includes, excludes) 265 | } 266 | 267 | fn main() { 268 | // Capacity estimate is a guess -- 4x as large as a SHA-256 hash seems 269 | // reasonable (and is a power of two). 270 | let mut buffer = Vec::with_capacity(256); 271 | let interesting_branches = interesting_branches(&mut buffer); 272 | let merge_bases = merge_bases(&mut buffer, &interesting_branches); 273 | let (includes, excludes) = includes_excludes(buffer, interesting_branches, &merge_bases); 274 | Command::new("git") 275 | .arg("log") 276 | .args(args_os().skip(1)) 277 | .args(includes) 278 | .arg("--not") 279 | .args(merge_bases.into_iter().map(|mut id| { 280 | id.push_str("^@"); 281 | id 282 | })) 283 | .args(excludes) 284 | .spawn() 285 | .expect("Failed to run git") 286 | .wait() 287 | .expect("failed to wait for git"); 288 | } 289 | --------------------------------------------------------------------------------