├── .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 |
--------------------------------------------------------------------------------