├── git-branchless ├── src │ ├── main.rs │ ├── lib.rs │ └── commands │ │ ├── wrap.rs │ │ ├── snapshot.rs │ │ └── repair.rs ├── tests │ ├── test_git.rs │ ├── test_repair.rs │ ├── test_eventlog.rs │ ├── test_branchless.rs │ └── test_bug_report.rs ├── LICENSE-MIT └── Cargo.toml ├── media └── git-sl.png ├── git-branchless-revset ├── build.rs ├── src │ ├── lib.rs │ ├── ast.rs │ ├── grammar.lalrpop │ └── resolve.rs ├── Cargo.toml └── LICENSE-MIT ├── git-branchless-lib ├── README.md ├── src │ ├── core │ │ ├── mod.rs │ │ ├── rewrite │ │ │ ├── mod.rs │ │ │ └── evolve.rs │ │ ├── task.rs │ │ └── gc.rs │ ├── lib.rs │ ├── git │ │ ├── mod.rs │ │ └── index.rs │ └── util.rs ├── LICENSE-MIT ├── bin │ └── testing │ │ ├── profile_changed_paths.rs │ │ └── regression_test_cherry_pick.rs ├── tests │ ├── test_snapshot.rs │ ├── test_rewrite_evolve.rs │ ├── test_git_run.rs │ └── test_eventlog.rs └── Cargo.toml ├── rust-toolchain.toml ├── git-branchless-hook ├── src │ └── main.rs ├── Cargo.toml └── LICENSE-MIT ├── git-branchless-init ├── src │ └── main.rs ├── Cargo.toml └── LICENSE-MIT ├── git-branchless-test ├── src │ ├── main.rs │ └── worker.rs ├── LICENSE-MIT └── Cargo.toml ├── .gitattributes ├── .vscode └── settings.json ├── git-branchless-query ├── src │ ├── main.rs │ └── lib.rs ├── Cargo.toml └── LICENSE-MIT ├── git-branchless-record ├── src │ └── main.rs ├── Cargo.toml └── LICENSE-MIT ├── git-branchless-submit ├── src │ └── main.rs ├── LICENSE-MIT ├── Cargo.toml └── examples │ └── dump_phabricator_dependencies.rs ├── git-branchless-smartlog ├── src │ └── main.rs ├── Cargo.toml └── LICENSE-MIT ├── .editorconfig ├── git-branchless-undo ├── src │ └── tui │ │ ├── mod.rs │ │ ├── cursive.rs │ │ └── testing.rs ├── Cargo.toml └── LICENSE-MIT ├── scm-bisect ├── README.md ├── proptest-regressions │ ├── basic.txt │ └── lib.txt ├── Cargo.toml ├── src │ ├── lib.rs │ └── testing.rs ├── examples │ ├── guessing_game.proptest-regressions │ └── guessing_game.rs └── LICENSE-MIT ├── .github ├── ISSUE_TEMPLATE │ ├── config.yml │ ├── documentation_request.yml │ └── bug_report.yml ├── dependabot.yml ├── workflows │ ├── nix-linux.yml │ ├── nix-macos.yml │ ├── windows.yml │ ├── macos.yml │ ├── generate-wiki-toc.yml │ ├── lint.yml │ ├── linux-git-devel.yml │ ├── release.yml │ └── linux.yml └── FUNDING.yml ├── .gitpod └── Dockerfile ├── .gitpod.yml ├── .gitignore ├── git-branchless-opts ├── Cargo.toml └── LICENSE-MIT ├── git-branchless-move ├── Cargo.toml └── LICENSE-MIT ├── flake.lock ├── CONTRIBUTING.md ├── git-branchless-navigation ├── Cargo.toml └── LICENSE-MIT ├── git-branchless-reword ├── Cargo.toml ├── LICENSE-MIT └── src │ └── dialoguer_edit.rs ├── .devcontainer ├── Dockerfile └── devcontainer.json ├── demos ├── demo_helpers.tcl ├── demo_undo_amend.sh ├── demo_undo_conflict_resolution.sh └── demo_helpers.sh ├── git-branchless-invoke ├── Cargo.toml └── LICENSE-MIT ├── LICENSE-MIT ├── flake.nix ├── Cargo.toml └── CODE_OF_CONDUCT.md /git-branchless/src/main.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | git_branchless::commands::main(); 3 | } 4 | -------------------------------------------------------------------------------- /media/git-sl.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arxanas/git-branchless/HEAD/media/git-sl.png -------------------------------------------------------------------------------- /git-branchless-revset/build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | lalrpop::process_root().unwrap(); 3 | } 4 | -------------------------------------------------------------------------------- /git-branchless-lib/README.md: -------------------------------------------------------------------------------- 1 | Supporting library for [git-branchless](https://github.com/arxanas/git-branchless). 2 | -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | # Current minimum-supported Rust version 3 | channel = "1.82" 4 | profile = "default" 5 | -------------------------------------------------------------------------------- /git-branchless-hook/src/main.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | git_branchless_invoke::invoke_subcommand_main(git_branchless_hook::command_main) 3 | } 4 | -------------------------------------------------------------------------------- /git-branchless-init/src/main.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | git_branchless_invoke::invoke_subcommand_main(git_branchless_init::command_main) 3 | } 4 | -------------------------------------------------------------------------------- /git-branchless-test/src/main.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | git_branchless_invoke::invoke_subcommand_main(git_branchless_test::command_main) 3 | } 4 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | Cargo.lock linguist-generated=true merge=binary 2 | flake.lock linguist-generated=true merge=binary 3 | CHANGELOG.md merge=union 4 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, 3 | "editor.formatOnType": true, 4 | "files.insertFinalNewline": true 5 | } 6 | -------------------------------------------------------------------------------- /git-branchless-query/src/main.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | git_branchless_invoke::invoke_subcommand_main(git_branchless_query::command_main) 3 | } 4 | -------------------------------------------------------------------------------- /git-branchless-record/src/main.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | git_branchless_invoke::invoke_subcommand_main(git_branchless_record::command_main) 3 | } 4 | -------------------------------------------------------------------------------- /git-branchless-submit/src/main.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | git_branchless_invoke::invoke_subcommand_main(git_branchless_submit::command_main) 3 | } 4 | -------------------------------------------------------------------------------- /git-branchless-smartlog/src/main.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | git_branchless_invoke::invoke_subcommand_main(git_branchless_smartlog::command_main) 3 | } 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | trim_trailing_whitespace = true 6 | 7 | [*.{rs,lalrpop}] 8 | indent_style = space 9 | indent_size = 4 10 | -------------------------------------------------------------------------------- /git-branchless-undo/src/tui/mod.rs: -------------------------------------------------------------------------------- 1 | //! Utilities to control output and render to the terminal. 2 | 3 | mod cursive; 4 | pub mod testing; 5 | 6 | pub use self::cursive::{with_siv, SingletonView}; 7 | -------------------------------------------------------------------------------- /scm-bisect/README.md: -------------------------------------------------------------------------------- 1 | ## scm-bisect 2 | 3 | Reusable algorithms for identifying the first bad commit in a directed acyclic 4 | graph (similar to `git-bisect`). The intention is to provide support for various 5 | source control systems. 6 | 7 | License: MIT. 8 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | contact_links: 2 | - name: Start a Discussion 3 | url: https://github.com/arxanas/git-branchless/discussions 4 | about: Ask questions, suggest features, brainstorm solutions, and so on. Open-ended topics belong here. 5 | -------------------------------------------------------------------------------- /.gitpod/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM gitpod/workspace-full 2 | 3 | # The following environment variables are necessary for `cargo test` to pass. 4 | # Determined by running `which git` and `git --exec-path`. 5 | ENV TEST_GIT=/usr/bin/git TEST_GIT_EXEC_PATH=/usr/lib/git-core 6 | -------------------------------------------------------------------------------- /.gitpod.yml: -------------------------------------------------------------------------------- 1 | image: 2 | file: .gitpod/Dockerfile 3 | tasks: 4 | - init: | 5 | rustup default 1.82 6 | cargo test --no-run 7 | cargo install cargo-insta 8 | cargo install git-branchless && git branchless init 9 | vscode: 10 | extensions: 11 | - "matklad.rust-analyzer" 12 | -------------------------------------------------------------------------------- /git-branchless-lib/src/core/mod.rs: -------------------------------------------------------------------------------- 1 | //! Core algorithms and data structures. 2 | 3 | pub mod check_out; 4 | pub mod config; 5 | pub mod dag; 6 | pub mod effects; 7 | pub mod eventlog; 8 | pub mod formatting; 9 | pub mod gc; 10 | pub mod node_descriptors; 11 | pub mod repo_ext; 12 | pub mod rewrite; 13 | pub mod task; 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | *.so 3 | *.pending-snap 4 | 5 | # For IntelliJ/Jetbrains software. You can get an open-source license for 6 | # Jetbrains products to develop git-branchless! File an issue and I will apply 7 | # on your behalf. 8 | .idea 9 | 10 | # Direnv: https://github.com/direnv/direnv 11 | /.direnv/ 12 | /.envrc 13 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | updates: 4 | - package-ecosystem: "cargo" 5 | directory: "/" 6 | schedule: 7 | interval: "weekly" 8 | commit-message: 9 | prefix: "build:" 10 | 11 | - package-ecosystem: "github-actions" 12 | directory: "/" 13 | schedule: 14 | interval: "weekly" 15 | commit-message: 16 | prefix: "build:" 17 | -------------------------------------------------------------------------------- /git-branchless-lib/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Core functionality for git-branchless. 2 | 3 | #![warn(missing_docs)] 4 | #![warn( 5 | clippy::all, 6 | clippy::as_conversions, 7 | clippy::clone_on_ref_ptr, 8 | clippy::dbg_macro 9 | )] 10 | #![allow(clippy::too_many_arguments, clippy::blocks_in_conditions)] 11 | 12 | pub mod core; 13 | pub mod git; 14 | pub mod testing; 15 | pub mod util; 16 | -------------------------------------------------------------------------------- /git-branchless-opts/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | description = "Supporting library for git-branchless" 3 | edition = "2021" 4 | license = "MIT OR Apache-2.0" 5 | name = "git-branchless-opts" 6 | repository = "https://github.com/arxanas/git-branchless" 7 | version = "0.10.0" 8 | 9 | [dependencies] 10 | clap = { workspace = true, features = ["derive"] } 11 | clap_mangen = { workspace = true } 12 | itertools = { workspace = true } 13 | lib = { workspace = true } 14 | scm-diff-editor = { workspace = true } 15 | -------------------------------------------------------------------------------- /scm-bisect/proptest-regressions/basic.txt: -------------------------------------------------------------------------------- 1 | # Seeds for failure cases proptest has generated in the past. It is 2 | # automatically read and these particular cases re-run before any 3 | # novel cases are generated. 4 | # 5 | # It is recommended to check this file in to source control so that 6 | # everyone who runs the test benefits from these saved cases. 7 | cc 5a4ffdf1267e9ca2dc25020bfaf35f8de38978291725aa4c7451dcde0868e8b8 # shrinks to strategy = Linear, (graph, failure_nodes) = (TestGraph { nodes: {'a': {}} }, []) 8 | -------------------------------------------------------------------------------- /scm-bisect/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | description = "Supporting library for git-branchless" 3 | edition = "2021" 4 | license = "MIT OR Apache-2.0" 5 | name = "scm-bisect" 6 | repository = "https://github.com/arxanas/git-branchless" 7 | version = "0.3.0" 8 | 9 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 10 | 11 | [dependencies] 12 | indexmap = "2" 13 | itertools = "0.14" 14 | thiserror = "2" 15 | tracing = "0.1" 16 | 17 | [dev-dependencies] 18 | insta = "1" 19 | maplit = "1" 20 | proptest = "1.7.0" 21 | -------------------------------------------------------------------------------- /scm-bisect/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Reusable algorithms for identifying the first bad commit in a directed 2 | //! acyclic graph (similar to `git-bisect`). The intention is to provide support 3 | //! for various source control systems. 4 | 5 | #![warn(missing_docs)] 6 | #![warn( 7 | clippy::all, 8 | clippy::as_conversions, 9 | clippy::clone_on_ref_ptr, 10 | clippy::dbg_macro 11 | )] 12 | #![allow(clippy::too_many_arguments, clippy::blocks_in_conditions)] 13 | 14 | pub mod basic; 15 | pub mod search; 16 | 17 | #[cfg(test)] 18 | pub mod testing; 19 | -------------------------------------------------------------------------------- /git-branchless-move/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | description = "Supporting library for git-branchless" 3 | edition = "2021" 4 | license = "MIT OR Apache-2.0" 5 | name = "git-branchless-move" 6 | repository = "https://github.com/arxanas/git-branchless" 7 | version = "0.10.0" 8 | 9 | [dependencies] 10 | eden_dag = { workspace = true } 11 | eyre = { workspace = true } 12 | git-branchless-revset = { workspace = true } 13 | git-branchless-opts = { workspace = true } 14 | lib = { workspace = true } 15 | rayon = { workspace = true } 16 | tracing = { workspace = true } 17 | 18 | [dev-dependencies] 19 | insta = { workspace = true } 20 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "nixpkgs": { 4 | "locked": { 5 | "lastModified": 1756636162, 6 | "narHash": "sha256-mBecwgUTWRgClJYqcF+y4O1bY8PQHqeDpB+zsAn+/zA=", 7 | "owner": "NixOS", 8 | "repo": "nixpkgs", 9 | "rev": "37ff64b7108517f8b6ba5705ee5085eac636a249", 10 | "type": "github" 11 | }, 12 | "original": { 13 | "id": "nixpkgs", 14 | "type": "indirect" 15 | } 16 | }, 17 | "root": { 18 | "inputs": { 19 | "nixpkgs": "nixpkgs" 20 | } 21 | } 22 | }, 23 | "root": "root", 24 | "version": 7 25 | } 26 | -------------------------------------------------------------------------------- /.github/workflows/nix-linux.yml: -------------------------------------------------------------------------------- 1 | name: Nix on Linux 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | 9 | jobs: 10 | nix: 11 | runs-on: ubuntu-latest 12 | name: nix-build 13 | timeout-minutes: 40 14 | steps: 15 | - uses: actions/checkout@v5 16 | with: 17 | fetch-depth: 0 18 | - uses: cachix/install-nix-action@v31 19 | with: 20 | extra_nix_config: | 21 | experimental-features = nix-command flakes 22 | access-tokens = github.com=${{ secrets.GITHUB_TOKEN }} 23 | - run: nix flake check --print-build-logs --show-trace 24 | -------------------------------------------------------------------------------- /git-branchless-smartlog/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | description = "Supporting library for git-branchless" 3 | edition = "2021" 4 | license = "MIT OR Apache-2.0" 5 | name = "git-branchless-smartlog" 6 | repository = "https://github.com/arxanas/git-branchless" 7 | version = "0.10.0" 8 | 9 | [dependencies] 10 | cursive_core = { workspace = true } 11 | eden_dag = { workspace = true } 12 | eyre = { workspace = true } 13 | git-branchless-invoke = { workspace = true } 14 | git-branchless-opts = { workspace = true } 15 | git-branchless-revset = { workspace = true } 16 | lib = { workspace = true } 17 | tracing = { workspace = true } 18 | 19 | [dev-dependencies] 20 | insta = { workspace = true } 21 | -------------------------------------------------------------------------------- /git-branchless-undo/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | description = "Supporting library for git-branchless" 3 | edition = "2021" 4 | license = "MIT OR Apache-2.0" 5 | name = "git-branchless-undo" 6 | repository = "https://github.com/arxanas/git-branchless" 7 | version = "0.10.0" 8 | 9 | [dependencies] 10 | cursive = { workspace = true } 11 | eyre = { workspace = true } 12 | lib = { workspace = true } 13 | git-branchless-revset = { workspace = true } 14 | git-branchless-smartlog = { workspace = true } 15 | tracing = { workspace = true } 16 | cursive_buffered_backend = { workspace = true } 17 | cursive_core = { workspace = true } 18 | 19 | [dev-dependencies] 20 | insta = { workspace = true } 21 | -------------------------------------------------------------------------------- /scm-bisect/proptest-regressions/lib.txt: -------------------------------------------------------------------------------- 1 | # Seeds for failure cases proptest has generated in the past. It is 2 | # automatically read and these particular cases re-run before any 3 | # novel cases are generated. 4 | # 5 | # It is recommended to check this file in to source control so that 6 | # everyone who runs the test benefits from these saved cases. 7 | cc ce11ffd995d0052397748bfc67c12868894333525e05f275b93791d554eed36c # shrinks to strategy = Linear, (graph, failure_nodes) = (TestGraph { nodes: {'a': {'¡'}} }, ['a']) 8 | cc d64864227683ebca9d4ac1a79649d8e51162bc55b3450269c39e842d8ee0f7dd # shrinks to strategy = Linear, (graph, failure_nodes) = (TestGraph { nodes: {'c': {}, 'a': {}} }, ['c']) 9 | -------------------------------------------------------------------------------- /scm-bisect/examples/guessing_game.proptest-regressions: -------------------------------------------------------------------------------- 1 | # Seeds for failure cases proptest has generated in the past. It is 2 | # automatically read and these particular cases re-run before any 3 | # novel cases are generated. 4 | # 5 | # It is recommended to check this file in to source control so that 6 | # everyone who runs the test benefits from these saved cases. 7 | cc 67760b0a17386062ba77f3c425a851fa115bcdcb9fbe2697c66e2a1867e600a7 # shrinks to inputs = [Less, Less, Less, Greater, Less, Less, Less] 8 | cc 30da0c32ee62618d38b87f1ef1fe8b135a6703ca4b5965e29da5ecf47f5860e9 # shrinks to input = 0 9 | cc f7618c6f857bd07b2c8f1b086c2ca175e73d1a6189b1d14d7ffb0f7ddd35ae68 # shrinks to input = 100 10 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Welcome to `git-branchless`! Please review the [code of conduct](/CODE_OF_CONDUCT.md) before participating in the project. 2 | 3 | # Bugs and feature requests 4 | 5 | You can report bugs in the [Github issue tracker](https://github.com/arxanas/git-branchless/issues). There is no formal issue template at this time. For bugs, please provide a [Short, Self-Contained, Correct Example](http://sscce.org/). 6 | 7 | For feature requests, workflow questions, and other open-ended topics, [start a discussion](https://github.com/arxanas/git-branchless/discussions). 8 | 9 | # Development 10 | 11 | See the developer guide in sidebar of [the wiki](https://github.com/arxanas/git-branchless/wiki). 12 | -------------------------------------------------------------------------------- /git-branchless-navigation/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | description = "Supporting library for git-branchless" 3 | edition = "2021" 4 | license = "MIT OR Apache-2.0" 5 | name = "git-branchless-navigation" 6 | repository = "https://github.com/arxanas/git-branchless" 7 | version = "0.10.0" 8 | 9 | [dependencies] 10 | cursive = { workspace = true } 11 | eden_dag = { workspace = true } 12 | eyre = { workspace = true } 13 | git-branchless-opts = { workspace = true } 14 | git-branchless-revset = { workspace = true } 15 | git-branchless-smartlog = { workspace = true } 16 | itertools = { workspace = true } 17 | lib = { workspace = true } 18 | tracing = { workspace = true } 19 | 20 | [target.'cfg(unix)'.dependencies] 21 | skim = { workspace = true } 22 | -------------------------------------------------------------------------------- /git-branchless-reword/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | description = "Supporting library for git-branchless" 3 | edition = "2021" 4 | license = "MIT OR Apache-2.0" 5 | name = "git-branchless-reword" 6 | repository = "https://github.com/arxanas/git-branchless" 7 | version = "0.10.0" 8 | 9 | [dependencies] 10 | bstr = { workspace = true } 11 | chrono = { workspace = true } 12 | eden_dag = { workspace = true } 13 | eyre = { workspace = true } 14 | git-branchless-opts = { workspace = true } 15 | git-branchless-revset = { workspace = true } 16 | lib = { workspace = true } 17 | rayon = { workspace = true } 18 | shell-words = { workspace = true } 19 | tempfile = { workspace = true } 20 | tracing = { workspace = true } 21 | 22 | [dev-dependencies] 23 | insta = { workspace = true } 24 | -------------------------------------------------------------------------------- /git-branchless-query/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | description = "Supporting library for git-branchless" 3 | edition = "2021" 4 | license = "MIT OR Apache-2.0" 5 | name = "git-branchless-query" 6 | repository = "https://github.com/arxanas/git-branchless" 7 | version = "0.10.0" 8 | 9 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 10 | 11 | [dependencies] 12 | eden_dag = { workspace = true } 13 | eyre = { workspace = true } 14 | git-branchless-invoke = { workspace = true } 15 | git-branchless-opts = { workspace = true } 16 | git-branchless-revset = { workspace = true } 17 | itertools = { workspace = true } 18 | lib = { workspace = true } 19 | tracing = { workspace = true } 20 | 21 | [dev-dependencies] 22 | insta = { workspace = true } 23 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [arxanas] 4 | # patreon: arxanas 5 | 6 | # open_collective: # Replace with a single Open Collective username 7 | # ko_fi: # Replace with a single Ko-fi username 8 | # tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 9 | # community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 10 | # liberapay: # Replace with a single Liberapay username 11 | # issuehunt: # Replace with a single IssueHunt username 12 | # otechie: # Replace with a single Otechie username 13 | # lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 14 | # custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 15 | -------------------------------------------------------------------------------- /.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | # Use Ubuntu as base since we're using features for Rust/Git/Nix 2 | FROM mcr.microsoft.com/devcontainers/base:ubuntu 3 | 4 | # Install git build dependencies and development tools that aren't available as features 5 | RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ 6 | && apt-get install -y \ 7 | # Git build dependencies for testing different Git versions 8 | dh-autoreconf \ 9 | libcurl4-gnutls-dev \ 10 | libexpat1-dev \ 11 | gettext \ 12 | libz-dev \ 13 | libssl-dev \ 14 | # Additional development tools 15 | build-essential \ 16 | pkg-config \ 17 | # Tools for testing different Git versions 18 | autoconf \ 19 | make \ 20 | libpcre2-dev 21 | -------------------------------------------------------------------------------- /git-branchless-hook/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | description = "Supporting library for git-branchless" 3 | edition = "2021" 4 | license = "MIT OR Apache-2.0" 5 | name = "git-branchless-hook" 6 | repository = "https://github.com/arxanas/git-branchless" 7 | version = "0.10.0" 8 | 9 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 10 | 11 | [dependencies] 12 | console = { workspace = true } 13 | eyre = { workspace = true } 14 | git-branchless-invoke = { workspace = true } 15 | git-branchless-opts = { workspace = true } 16 | itertools = { workspace = true } 17 | lazy_static = { workspace = true } 18 | lib = { workspace = true } 19 | regex = { workspace = true } 20 | tracing = { workspace = true } 21 | 22 | [dev-dependencies] 23 | insta = { workspace = true } 24 | -------------------------------------------------------------------------------- /demos/demo_helpers.tcl: -------------------------------------------------------------------------------- 1 | set send_slow {1 0.2} 2 | set send_human {0.1 0.3 1 0.05 1} 3 | set timeout 1 4 | set CTRLC \003 5 | set ESC \033 6 | 7 | proc expect_prompt {} { 8 | expect "$ " 9 | } 10 | 11 | proc run_command {cmd} { 12 | send -h "$cmd" 13 | sleep 3 14 | send "\r" 15 | expect -timeout 1 16 | } 17 | 18 | proc send_keystroke_to_interactive_process {key {addl_sleep 2}} { 19 | send "$key" 20 | expect -timeout 1 21 | sleep $addl_sleep 22 | } 23 | 24 | proc quit_and_dump_asciicast_path {} { 25 | set CTRLC \003 26 | set ESC \033 27 | 28 | send "exit\r" 29 | expect "asciinema: recording finished" 30 | sleep 1 31 | send $CTRLC 32 | expect -re "asciicast saved to (.+)$ESC.*\r" { 33 | send_user "$expect_out(1,string)\n" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /git-branchless-init/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | description = "Supporting library for git-branchless" 3 | edition = "2021" 4 | license = "MIT OR Apache-2.0" 5 | name = "git-branchless-init" 6 | repository = "https://github.com/arxanas/git-branchless" 7 | version = "0.10.0" 8 | 9 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 10 | 11 | [dependencies] 12 | console = { workspace = true } 13 | eyre = { workspace = true } 14 | git-branchless-invoke = { workspace = true } 15 | git-branchless-opts = { workspace = true } 16 | itertools = { workspace = true } 17 | lib = { workspace = true } 18 | path-slash = { workspace = true } 19 | tracing = { workspace = true } 20 | 21 | [dev-dependencies] 22 | assert_cmd = { workspace = true } 23 | 24 | [features] 25 | man-pages = [] 26 | -------------------------------------------------------------------------------- /git-branchless-revset/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Parser and evaluator for a "revset"-like language, as in Mercurial and 2 | //! Jujutsu. 3 | 4 | #![warn(missing_docs)] 5 | #![warn( 6 | clippy::all, 7 | clippy::as_conversions, 8 | clippy::clone_on_ref_ptr, 9 | clippy::dbg_macro 10 | )] 11 | #![allow(clippy::too_many_arguments, clippy::blocks_in_conditions)] 12 | 13 | mod ast; 14 | mod builtins; 15 | mod eval; 16 | mod parser; 17 | mod pattern; 18 | mod resolve; 19 | 20 | pub use ast::Expr; 21 | pub use eval::eval; 22 | pub use parser::parse; 23 | pub use resolve::{check_revset_syntax, resolve_commits, resolve_default_smartlog_commits}; 24 | 25 | use lalrpop_util::lalrpop_mod; 26 | lalrpop_mod!( 27 | #[allow(clippy::all, clippy::as_conversions, dead_code)] 28 | grammar, 29 | "/grammar.rs" 30 | ); 31 | -------------------------------------------------------------------------------- /.github/workflows/nix-macos.yml: -------------------------------------------------------------------------------- 1 | name: Nix on macOS 2 | 3 | on: 4 | schedule: 5 | # Run once every day at 6:40AM UTC. 6 | - cron: "40 6 * * *" 7 | 8 | push: 9 | branches: 10 | - master 11 | 12 | pull_request: 13 | 14 | jobs: 15 | nix: 16 | if: startsWith(github.head_ref, 'ci-') || github.head_ref == '' 17 | runs-on: macos-latest 18 | 19 | name: nix-build 20 | timeout-minutes: 40 21 | steps: 22 | - uses: actions/checkout@v5 23 | with: 24 | fetch-depth: 0 25 | - uses: cachix/install-nix-action@v31 26 | with: 27 | extra_nix_config: | 28 | experimental-features = nix-command flakes 29 | access-tokens = github.com=${{ secrets.GITHUB_TOKEN }} 30 | - run: nix flake check --print-build-logs --show-trace 31 | -------------------------------------------------------------------------------- /git-branchless-invoke/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | description = "Supporting library for git-branchless" 3 | edition = "2021" 4 | license = "MIT OR Apache-2.0" 5 | name = "git-branchless-invoke" 6 | repository = "https://github.com/arxanas/git-branchless" 7 | version = "0.10.0" 8 | 9 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 10 | 11 | [dependencies] 12 | clap = { workspace = true, features = ["derive"] } 13 | color-eyre = { workspace = true } 14 | cursive_core = { workspace = true } 15 | eyre = { workspace = true } 16 | git-branchless-opts = { workspace = true } 17 | git2 = { workspace = true } 18 | lib = { workspace = true } 19 | tracing = { workspace = true } 20 | tracing-chrome = { workspace = true } 21 | tracing-error = { workspace = true } 22 | tracing-subscriber = { workspace = true } 23 | -------------------------------------------------------------------------------- /git-branchless-record/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | description = "Supporting library for git-branchless" 3 | edition = "2021" 4 | license = "MIT OR Apache-2.0" 5 | name = "git-branchless-record" 6 | repository = "https://github.com/arxanas/git-branchless" 7 | version = "0.10.0" 8 | 9 | [dependencies] 10 | cursive = { version = "0.20.0", default-features = false, features = [ 11 | "crossterm-backend", 12 | ] } 13 | cursive_buffered_backend = { workspace = true } 14 | eden_dag = { workspace = true } 15 | eyre = { workspace = true } 16 | git-branchless-invoke = { workspace = true } 17 | git-branchless-opts = { workspace = true } 18 | git-branchless-reword = { workspace = true } 19 | itertools = { workspace = true } 20 | lib = { workspace = true } 21 | rayon = { workspace = true } 22 | scm-record = { workspace = true } 23 | tracing = { workspace = true } 24 | 25 | [dev-dependencies] 26 | insta = { workspace = true } 27 | -------------------------------------------------------------------------------- /git-branchless/tests/test_git.rs: -------------------------------------------------------------------------------- 1 | use eyre::WrapErr; 2 | use lib::testing::{make_git, GitRunOptions}; 3 | 4 | #[test] 5 | fn test_git_is_not_a_wrapper() -> eyre::Result<()> { 6 | let git = make_git()?; 7 | { 8 | let (_stdout, stderr) = git 9 | .run_with_options( 10 | &["config", "--global", "--list"], 11 | &GitRunOptions { 12 | expected_exit_code: 128, 13 | ..Default::default() 14 | }, 15 | ) 16 | .wrap_err( 17 | "The Git global configuration should not exist during tests, \ 18 | as the HOME environment variable is not set. \ 19 | Check that the Git executable is not being wrapped in a shell script.", 20 | )?; 21 | insta::assert_snapshot!(stderr, @r###" 22 | fatal: $HOME not set 23 | "###); 24 | } 25 | Ok(()) 26 | } 27 | -------------------------------------------------------------------------------- /.github/workflows/windows.yml: -------------------------------------------------------------------------------- 1 | name: Windows 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | 9 | env: 10 | CARGO_INCREMENTAL: 0 11 | RUST_BACKTRACE: short 12 | 13 | jobs: 14 | run-tests: 15 | runs-on: windows-latest 16 | steps: 17 | - uses: actions/checkout@v5 18 | 19 | - name: Set up Rust 20 | uses: actions-rs/toolchain@v1 21 | with: 22 | profile: minimal 23 | toolchain: 1.82 24 | override: true 25 | 26 | - name: Cache dependencies 27 | uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 28 | 29 | - name: Compile 30 | run: cargo build --all-targets --workspace 31 | 32 | # TODO(#1416): re-enable once tests are passing on Git v2.46+ 33 | # - name: Run tests 34 | # timeout-minutes: 30 35 | # run: | 36 | # $env:TEST_GIT='C:\Program Files\Git\cmd\git.exe' 37 | # $env:TEST_GIT_EXEC_PATH='C:\Program Files\Git\cmd' 38 | # cargo test --examples --tests --workspace --no-fail-fast 39 | -------------------------------------------------------------------------------- /demos/demo_undo_amend.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail 3 | . "$(dirname "$0")"/demo_helpers.sh 4 | 5 | parse_args "$@" 6 | set_up_git_repo 7 | echo 'Hello, world!' >foo 8 | git add foo 9 | git commit -m 'Commit foo' 10 | 11 | run_demo ' 12 | run_command "cat foo" 13 | expect_prompt 14 | 15 | run_command "vim foo" 16 | sleep 1 17 | send_keystroke_to_interactive_process "C" 18 | send -h "Goodbye, world!" 19 | sleep 1 20 | send -h \x03 21 | sleep 1 22 | send -h ":wq\r" 23 | sleep 1 24 | expect_prompt 25 | 26 | run_command "git add foo" 27 | run_command "git commit --amend" 28 | send_keystroke_to_interactive_process "C" 29 | send -h "Amend foo bad" 30 | sleep 1 31 | send -h \x03 32 | sleep 1 33 | send -h ":wq\r" 34 | sleep 1 35 | expect_prompt 36 | 37 | run_command "cat foo" 38 | run_command "echo oh no" 39 | 40 | run_command "git undo" 41 | expect -timeout 3 42 | send_keystroke_to_interactive_process "p" 2 43 | send_keystroke_to_interactive_process "\r" 1 44 | expect "Confirm?" 45 | run_command "y" 46 | 47 | run_command "cat foo" 48 | run_command "echo crisis averted" 49 | ' 50 | -------------------------------------------------------------------------------- /git-branchless/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Branchless workflow for Git. 2 | //! 3 | //! # Why? 4 | //! 5 | //! Most Git workflows involve heavy use of branches to track commit work that is 6 | //! underway. However, branches require that you "name" every commit you're 7 | //! interested in tracking. If you spend a lot of time doing any of the following: 8 | //! 9 | //! * Switching between work tasks. 10 | //! * Separating minor cleanups/refactorings into their own commits, for ease of 11 | //! reviewability. 12 | //! * Performing speculative work which may not be ultimately committed. 13 | //! * Working on top of work that you or a collaborator produced, which is not 14 | //! yet checked in. 15 | //! * Losing track of `git stash`es you made previously. 16 | //! 17 | //! Then the branchless workflow may be for you instead. 18 | 19 | #![warn(missing_docs)] 20 | #![warn( 21 | clippy::all, 22 | clippy::as_conversions, 23 | clippy::clone_on_ref_ptr, 24 | clippy::dbg_macro 25 | )] 26 | #![allow(clippy::too_many_arguments, clippy::blocks_in_conditions)] 27 | 28 | pub mod commands; 29 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/documentation_request.yml: -------------------------------------------------------------------------------- 1 | name: Documentation issue 2 | description: | 3 | Report unclear documentation or request missing documentation. 4 | labels: ["documentation"] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | Thanks for reporting a documentation issue! High-quality documentation is a [first-class goal](https://github.com/arxanas/git-branchless/wiki/Design-goals) for git-branchless, so your report is appreciated. 10 | 11 | - type: input 12 | id: url 13 | attributes: 14 | label: Wiki page 15 | description: The URL of the Wiki page, if applicable. 16 | placeholder: https://github.com/arxanas/git-branchless/wiki/Design-goals 17 | 18 | - type: input 19 | id: command 20 | attributes: 21 | label: Subcommand 22 | description: The specific `git-branchless` subcommand(s) which the request pertains to, if applicable. 23 | placeholder: next, prev 24 | 25 | - type: textarea 26 | id: description 27 | attributes: 28 | label: Description 29 | description: Description of the issue. 30 | -------------------------------------------------------------------------------- /git-branchless-revset/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | description = "Supporting library for git-branchless" 3 | edition = "2021" 4 | license = "MIT OR Apache-2.0" 5 | name = "git-branchless-revset" 6 | repository = "https://github.com/arxanas/git-branchless" 7 | version = "0.10.0" 8 | 9 | [dependencies] 10 | bstr = { workspace = true } 11 | chrono = { workspace = true } 12 | chrono-english = { workspace = true } 13 | chronoutil = { workspace = true } 14 | eden_dag = { workspace = true } 15 | eyre = { workspace = true } 16 | futures = { workspace = true } 17 | git-branchless-opts = { workspace = true } 18 | glob = { workspace = true } 19 | itertools = { workspace = true } 20 | lalrpop = { workspace = true } 21 | lalrpop-util = { workspace = true } 22 | lazy_static = { workspace = true } 23 | lib = { workspace = true } 24 | rayon = { workspace = true } 25 | regex = { workspace = true } 26 | serde_json = { workspace = true } 27 | thiserror = { workspace = true } 28 | tracing = { workspace = true } 29 | 30 | [build-dependencies] 31 | lalrpop = { workspace = true } 32 | 33 | [dev-dependencies] 34 | insta = { workspace = true } 35 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2021 Individual contributors 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /scm-bisect/LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2021 Individual contributors 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /git-branchless-lib/LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2021 Individual contributors 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /git-branchless/LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2021 Individual contributors 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /git-branchless-hook/LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2021 Individual contributors 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /git-branchless-init/LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2021 Individual contributors 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /git-branchless-invoke/LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2021 Individual contributors 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /git-branchless-move/LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2021 Individual contributors 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /git-branchless-opts/LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2021 Individual contributors 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /git-branchless-query/LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2021 Individual contributors 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /git-branchless-record/LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2021 Individual contributors 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /git-branchless-revset/LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2021 Individual contributors 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /git-branchless-reword/LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2021 Individual contributors 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /git-branchless-smartlog/LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2021 Individual contributors 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /git-branchless-submit/LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2021 Individual contributors 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /git-branchless-test/LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2021 Individual contributors 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /git-branchless-undo/LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2021 Individual contributors 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /git-branchless-navigation/LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2021 Individual contributors 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /git-branchless-submit/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | description = "Supporting library for git-branchless" 3 | edition = "2021" 4 | license = "MIT OR Apache-2.0" 5 | name = "git-branchless-submit" 6 | repository = "https://github.com/arxanas/git-branchless" 7 | version = "0.10.0" 8 | 9 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 10 | 11 | [dependencies] 12 | cursive_core = { workspace = true } 13 | eden_dag = { workspace = true } 14 | eyre = { workspace = true } 15 | git-branchless-invoke = { workspace = true } 16 | git-branchless-opts = { workspace = true } 17 | git-branchless-revset = { workspace = true } 18 | git-branchless-test = { workspace = true } 19 | indexmap = { workspace = true } 20 | itertools = { workspace = true } 21 | lazy_static = { workspace = true } 22 | lib = { workspace = true } 23 | rayon = { workspace = true } 24 | regex = { workspace = true } 25 | serde = { workspace = true, features = ["derive"] } 26 | serde_json = { workspace = true } 27 | tempfile = { workspace = true } 28 | thiserror = { workspace = true } 29 | tracing = { workspace = true } 30 | 31 | [dev-dependencies] 32 | clap = { workspace = true } 33 | insta = { workspace = true } 34 | -------------------------------------------------------------------------------- /git-branchless-test/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | description = "Supporting library for git-branchless" 3 | edition = "2021" 4 | license = "MIT OR Apache-2.0" 5 | name = "git-branchless-test" 6 | repository = "https://github.com/arxanas/git-branchless" 7 | version = "0.10.0" 8 | 9 | [dependencies] 10 | bstr = { workspace = true } 11 | clap = { workspace = true } 12 | crossbeam = { workspace = true } 13 | cursive = { workspace = true } 14 | eden_dag = { workspace = true } 15 | eyre = { workspace = true } 16 | fslock = { workspace = true } 17 | git-branchless-invoke = { workspace = true } 18 | git-branchless-opts = { workspace = true } 19 | git-branchless-revset = { workspace = true } 20 | indexmap = { workspace = true } 21 | itertools = { workspace = true } 22 | lazy_static = { workspace = true } 23 | lib = { workspace = true } 24 | num_cpus = { workspace = true } 25 | rayon = { workspace = true } 26 | scm-bisect = { workspace = true } 27 | serde = { workspace = true } 28 | serde_json = { workspace = true } 29 | tempfile = { workspace = true } 30 | thiserror = { workspace = true } 31 | tracing = { workspace = true } 32 | 33 | [dev-dependencies] 34 | assert_cmd = { workspace = true } 35 | insta = { workspace = true } 36 | maplit = { workspace = true } 37 | -------------------------------------------------------------------------------- /.github/workflows/macos.yml: -------------------------------------------------------------------------------- 1 | name: macOS 2 | 3 | on: 4 | schedule: 5 | # Run once every day at 6:40AM UTC. 6 | - cron: "40 6 * * *" 7 | 8 | push: 9 | branches: 10 | - master 11 | 12 | pull_request: 13 | 14 | env: 15 | CARGO_INCREMENTAL: 0 16 | RUST_BACKTRACE: short 17 | 18 | jobs: 19 | run-tests: 20 | if: contains(github.head_ref, 'macos') || github.head_ref == '' 21 | runs-on: macos-latest 22 | 23 | steps: 24 | - uses: actions/checkout@v5 25 | 26 | - name: Set up Rust 27 | uses: actions-rs/toolchain@v1 28 | with: 29 | profile: minimal 30 | toolchain: 1.82 31 | override: true 32 | 33 | - name: Cache dependencies 34 | uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 35 | 36 | - name: Compile 37 | run: cargo build --benches --tests 38 | 39 | # TODO(#1416): re-enable once tests are passing on Git v2.46+ 40 | # - name: Run tests 41 | # timeout-minutes: 30 42 | # run: | 43 | # export RUST_BACKTRACE=1 44 | # export TEST_GIT=$(which git) 45 | # export TEST_GIT_EXEC_PATH=$("$TEST_GIT" --exec-path) 46 | # cargo test --workspace --no-fail-fast 47 | -------------------------------------------------------------------------------- /demos/demo_undo_conflict_resolution.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail 3 | . "$(dirname "$0")"/demo_helpers.sh 4 | 5 | parse_args "$@" 6 | set_up_git_repo 7 | echo 'Hello, world!' >foo 8 | git add foo 9 | git commit -m 'Commit foo' 10 | git checkout -b 'conflict' HEAD~ 11 | echo 'Goodbye, world!' >foo 12 | git add foo 13 | git commit -m 'Also commit foo' 14 | 15 | run_demo ' 16 | run_command "cat foo" 17 | expect_prompt 18 | 19 | run_command "git show master:foo" 20 | expect_prompt 21 | 22 | run_command "git rebase master" 23 | 24 | run_command "vim foo" 25 | sleep 1 26 | send_keystroke_to_interactive_process "V" 27 | send_keystroke_to_interactive_process "G" 28 | send_keystroke_to_interactive_process "C" 29 | send -h "Bad merge conflict resolution" 30 | sleep 1 31 | send -h \x03 32 | sleep 1 33 | send -h ":wq\r" 34 | sleep 1 35 | expect_prompt 36 | 37 | run_command "git add foo" 38 | run_command "git rebase --continue" 39 | send_keystroke_to_interactive_process "C" 40 | send -h "Bad merge" 41 | sleep 1 42 | send -h \x03 43 | sleep 1 44 | send -h ":wq\r" 45 | sleep 1 46 | expect_prompt 47 | 48 | run_command "cat foo" 49 | run_command "echo oh no" 50 | 51 | run_command "git undo" 52 | expect -timeout 3 53 | send_keystroke_to_interactive_process "p" 2 54 | send_keystroke_to_interactive_process "\r" 1 55 | expect "Confirm?" 56 | run_command "y" 57 | 58 | run_command "cat foo" 59 | run_command "echo crisis averted" 60 | ' 61 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "git-branchless", 3 | "build": { 4 | "dockerfile": "Dockerfile" 5 | }, 6 | 7 | // Features to add to the dev container. More info: https://containers.dev/features. 8 | "features": { 9 | "ghcr.io/devcontainers/features/rust:1": { 10 | "version": "1.81", 11 | "profile": "default" 12 | }, 13 | "ghcr.io/devcontainers/features/git:1": { 14 | "ppa": true, 15 | "version": "2.37.3" 16 | }, 17 | "ghcr.io/devcontainers/features/nix:1": { 18 | "multiUser": false, 19 | "version": "2.24" 20 | } 21 | }, 22 | 23 | // Configure tool-specific properties. 24 | "customizations": { 25 | // Configure properties specific to VS Code. 26 | "vscode": { 27 | // Add the IDs of extensions you want installed when the container is created. 28 | "extensions": [ 29 | "rust-lang.rust-analyzer", 30 | "tamasfe.even-better-toml", 31 | "ms-vscode.vscode-json", 32 | "ms-vscode.vscode-markdown" 33 | ], 34 | "settings": { 35 | "terminal.integrated.shellIntegration.enabled": true 36 | } 37 | } 38 | }, 39 | 40 | // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. 41 | "remoteUser": "vscode" 42 | } 43 | -------------------------------------------------------------------------------- /.github/workflows/generate-wiki-toc.yml: -------------------------------------------------------------------------------- 1 | name: "Generate Wiki tables of contents" 2 | 3 | on: 4 | - gollum 5 | - workflow_dispatch 6 | 7 | jobs: 8 | "generate-toc": 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v5 12 | with: 13 | repository: ${{ github.repository }}.wiki 14 | token: ${{ secrets.WIKI_UPDATE_TOC_TOKEN }} 15 | 16 | - run: | 17 | curl -O https://raw.githubusercontent.com/ekalinin/github-markdown-toc/17ec0170cdb8ac5c6fc4aa265087a1067e930b0a/gh-md-toc 18 | chmod +x gh-md-toc 19 | 20 | # Avoid recursive Actions invocations. 21 | if git show -s --format='%B' | egrep '^Updated '; then 22 | # Make sure to insert tables of contents individually, so that they 23 | # don't include the name of the document in the item links. 24 | for i in $(grep -l '' *.md); do ./gh-md-toc --insert --no-backup "$i"; done 25 | fi 26 | 27 | - name: Remove timestamps 28 | uses: jacobtomlinson/gha-find-replace@v3 29 | with: 30 | find: "" 31 | replace: "" 32 | include: "*.md" 33 | regex: true 34 | 35 | - uses: stefanzweifel/git-auto-commit-action@v6 36 | with: 37 | commit_message: Auto update Wiki tables of contents 38 | file_pattern: "*.md" 39 | -------------------------------------------------------------------------------- /git-branchless-lib/src/git/mod.rs: -------------------------------------------------------------------------------- 1 | //! Tools for interfacing with the Git repository. 2 | 3 | mod config; 4 | mod diff; 5 | mod index; 6 | mod object; 7 | mod oid; 8 | mod reference; 9 | mod repo; 10 | mod run; 11 | mod snapshot; 12 | mod status; 13 | mod test; 14 | mod tree; 15 | 16 | pub use config::{Config, ConfigRead, ConfigValue, ConfigWrite}; 17 | pub use diff::{process_diff_for_record, summarize_diff_for_temporary_commit, Diff}; 18 | pub use index::{update_index, Index, IndexEntry, Stage, UpdateIndexCommand}; 19 | pub use object::Commit; 20 | pub use oid::{MaybeZeroOid, NonZeroOid}; 21 | pub use reference::{ 22 | Branch, BranchType, CategorizedReferenceName, Reference, ReferenceName, ReferenceTarget, 23 | }; 24 | pub use repo::{ 25 | message_prettify, AmendFastOptions, CherryPickFastOptions, CreateCommitFastError, 26 | Error as RepoError, GitErrorCode, GitVersion, PatchId, Repo, ResolvedReferenceInfo, 27 | Result as RepoResult, Time, 28 | }; 29 | pub use run::{GitRunInfo, GitRunOpts, GitRunResult}; 30 | pub use snapshot::{WorkingCopyChangesType, WorkingCopySnapshot}; 31 | pub use status::{FileMode, FileStatus, StatusEntry}; 32 | pub use test::{ 33 | get_latest_test_command_path, get_test_locks_dir, get_test_tree_dir, get_test_worktrees_dir, 34 | make_test_command_slug, SerializedNonZeroOid, SerializedTestResult, TestCommand, 35 | TEST_ABORT_EXIT_CODE, TEST_INDETERMINATE_EXIT_CODE, TEST_SUCCESS_EXIT_CODE, 36 | }; 37 | pub use tree::{ 38 | dehydrate_tree, get_changed_paths_between_trees, hydrate_tree, make_empty_tree, Tree, 39 | }; 40 | -------------------------------------------------------------------------------- /git-branchless-lib/bin/testing/profile_changed_paths.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | use branchless::git::{NonZeroOid, Repo}; 4 | use tracing_chrome::ChromeLayerBuilder; 5 | use tracing_error::ErrorLayer; 6 | use tracing_subscriber::prelude::*; 7 | 8 | fn main() -> eyre::Result<()> { 9 | color_eyre::install()?; 10 | 11 | if std::env::var("RUST_PROFILE").is_ok() { 12 | let include_args = std::env::var("RUST_PROFILE_INCLUDE_ARGS").is_ok(); 13 | let (profile_layer, _profile_layer_guard) = 14 | ChromeLayerBuilder::new().include_args(include_args).build(); 15 | 16 | tracing_subscriber::registry() 17 | .with(ErrorLayer::default()) 18 | .with(profile_layer) 19 | .try_init()?; 20 | } 21 | 22 | let path_to_repo: PathBuf = std::env::var("PATH_TO_REPO") 23 | .expect("No `PATH_TO_REPO` was set") 24 | .into(); 25 | println!("Path to repo: {path_to_repo:?}"); 26 | 27 | let repo = Repo::from_dir(&path_to_repo)?; 28 | let commit = match std::env::var("COMMIT_OID") { 29 | Ok(commit_oid) => { 30 | let commit_oid: NonZeroOid = commit_oid.parse()?; 31 | repo.find_commit_or_fail(commit_oid)? 32 | } 33 | Err(_) => { 34 | let head_oid = repo 35 | .get_head_info()? 36 | .oid 37 | .expect("No `COMMIT_OID` was set, and no `HEAD` OID is available"); 38 | repo.find_commit_or_fail(head_oid)? 39 | } 40 | }; 41 | println!("Commit to check: {:?}", &commit); 42 | 43 | let result = repo.get_paths_touched_by_commit(&commit)?; 44 | println!("Result: {result:?}"); 45 | 46 | Ok(()) 47 | } 48 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | pull_request: 9 | 10 | env: 11 | CARGO_INCREMENTAL: 0 12 | RUST_BACKTRACE: short 13 | 14 | jobs: 15 | static-analysis: 16 | runs-on: ubuntu-latest 17 | 18 | steps: 19 | - uses: actions/checkout@v5 20 | 21 | - name: Forbid nocommit string 22 | run: | 23 | if results=$(git grep --column --line-number --only-matching '@''nocommit'); then 24 | echo "$results" 25 | awk <<<"$results" -F ':' '{ print "::error file=" $1 ",line=" $2 ",col=" $3 "::Illegal string: " $4 }' 26 | exit 1 27 | fi 28 | 29 | - name: Set up Rust 30 | uses: actions-rs/toolchain@v1 31 | with: 32 | profile: minimal 33 | toolchain: 1.82 34 | components: rustfmt, clippy 35 | override: true 36 | 37 | - name: Cache dependencies 38 | uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 39 | 40 | - name: Run `cargo fmt` 41 | uses: actions-rs/cargo@v1 42 | with: 43 | command: fmt 44 | args: --all -- --check 45 | 46 | - name: Run `cargo clippy` 47 | uses: actions-rs/cargo@v1 48 | with: 49 | command: clippy 50 | # Allow `--allow renamed_and_removed_lints` because the new lint name 51 | # may not be available in our oldest-supported Rust version. 52 | args: --workspace --all-features --all-targets -- --deny warnings --allow renamed_and_removed_lints 53 | 54 | - name: Run `cargo doc` 55 | env: 56 | RUSTDOCFLAGS: "--deny warnings" 57 | run: cargo doc --workspace --no-deps 58 | -------------------------------------------------------------------------------- /git-branchless-revset/src/ast.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | use std::collections::HashMap; 3 | use std::fmt::Display; 4 | 5 | /// A node in the parsed AST. 6 | #[allow(missing_docs)] 7 | #[derive(Clone, Debug)] 8 | pub enum Expr<'input> { 9 | Name(Cow<'input, str>), 10 | FunctionCall(Cow<'input, str>, Vec>), 11 | } 12 | 13 | impl Display for Expr<'_> { 14 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 15 | match self { 16 | Expr::Name(name) => write!(f, "{name}"), 17 | Expr::FunctionCall(name, args) => { 18 | write!(f, "{name}(")?; 19 | for (i, arg) in args.iter().enumerate() { 20 | if i > 0 { 21 | write!(f, ", ")?; 22 | } 23 | write!(f, "{arg}")?; 24 | } 25 | write!(f, ")") 26 | } 27 | } 28 | } 29 | } 30 | 31 | impl<'input> Expr<'input> { 32 | /// Replace names in this expression with arbitrary expressions. 33 | /// 34 | /// Given a HashMap of names to Expr's, build a new Expr by crawling this 35 | /// one and replacing any names contained in the map with the corresponding 36 | /// Expr. 37 | pub fn replace_names(&self, map: &HashMap>) -> Expr<'input> { 38 | match self { 39 | Expr::Name(name) => match map.get(&name.to_string()) { 40 | Some(expr) => expr.clone(), 41 | None => self.clone(), 42 | }, 43 | Expr::FunctionCall(name, args) => { 44 | let args = args.iter().map(|arg| arg.replace_names(map)).collect(); 45 | Expr::FunctionCall(name.clone(), args) 46 | } 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /git-branchless-lib/tests/test_snapshot.rs: -------------------------------------------------------------------------------- 1 | use std::time::SystemTime; 2 | 3 | use branchless::core::effects::Effects; 4 | use branchless::core::eventlog::EventLogDb; 5 | use branchless::core::formatting::Glyphs; 6 | use branchless::git::WorkingCopyChangesType; 7 | use branchless::testing::{make_git, GitRunOptions}; 8 | 9 | #[test] 10 | fn test_has_conflicts() -> eyre::Result<()> { 11 | let git = make_git()?; 12 | git.init_repo()?; 13 | 14 | git.commit_file("test1", 1)?; 15 | git.commit_file("test2", 2)?; 16 | git.run(&["checkout", "HEAD^"])?; 17 | git.commit_file_with_contents("test2", 2, "conflicting contents")?; 18 | 19 | git.run_with_options( 20 | &["merge", "master"], 21 | &GitRunOptions { 22 | expected_exit_code: 1, 23 | ..Default::default() 24 | }, 25 | )?; 26 | 27 | let glyphs = Glyphs::text(); 28 | let effects = Effects::new_suppress_for_test(glyphs); 29 | let git_run_info = git.get_git_run_info(); 30 | let repo = git.get_repo()?; 31 | let index = repo.get_index()?; 32 | let conn = repo.get_db_conn()?; 33 | let event_log_db = EventLogDb::new(&conn)?; 34 | let event_tx_id = event_log_db.make_transaction_id(SystemTime::now(), "testing")?; 35 | let head_info = repo.get_head_info()?; 36 | let (snapshot, status) = repo.get_status( 37 | &effects, 38 | &git_run_info, 39 | &index, 40 | &head_info, 41 | Some(event_tx_id), 42 | )?; 43 | insta::assert_debug_snapshot!(status, @r###" 44 | [ 45 | StatusEntry { 46 | index_status: Unmerged, 47 | working_copy_status: Added, 48 | working_copy_file_mode: Blob, 49 | path: "test2.txt", 50 | orig_path: None, 51 | }, 52 | ] 53 | "###); 54 | assert_eq!( 55 | snapshot.get_working_copy_changes_type()?, 56 | WorkingCopyChangesType::Conflicts 57 | ); 58 | 59 | Ok(()) 60 | } 61 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: File a bug report 2 | description: | 3 | This form is for bugs with concrete resolutions. If you don't have a specific resolution in mind, then start a Discussion instead. 4 | labels: 5 | - bug 6 | 7 | body: 8 | - type: markdown 9 | attributes: 10 | value: | 11 | Thanks for reporting a bug! This form is for bugs which have concrete resolutions. To ask a question or to brainstorm solutions to a problem, start a [Discussion](https://github.com/arxanas/git-branchless/discussions) instead. 12 | 13 | - type: textarea 14 | id: description 15 | attributes: 16 | label: Description of the bug 17 | description: Include a description of the bug and steps to reproduce the issue. 18 | validations: 19 | required: true 20 | 21 | - type: textarea 22 | id: expected-behavior 23 | attributes: 24 | label: Expected behavior 25 | description: What did you expect to happen? 26 | 27 | - type: textarea 28 | id: actual-behavior 29 | attributes: 30 | label: Actual behavior 31 | description: What actually happened? 32 | 33 | - type: input 34 | id: rustc-version 35 | attributes: 36 | label: Version of `rustc` 37 | description: If this is a build issue, paste the output of `rustc --version` here. 38 | placeholder: "Example: rustc 1.55.0 (c8dfcfe04 2021-09-06)" 39 | 40 | - type: textarea 41 | id: bug-report-output 42 | attributes: 43 | label: Automated bug report 44 | description: | 45 | Paste the output of `git branchless bug-report` here. 46 | If that subcommand doesn't exist, or if you don't want to share its output, leave this field blank. 47 | 48 | - type: input 49 | id: git-branchless-version 50 | attributes: 51 | label: Version of `git-branchless` 52 | description: | 53 | Paste the output of `git branchless --version` here. Only necessary if you didn't use `git branchless bug-report`. 54 | placeholder: "Example: git-branchless 0.3.7" 55 | 56 | - type: input 57 | id: git-version 58 | attributes: 59 | label: Version of `git` 60 | description: | 61 | Paste the output of `git version` here. Only necessary if you didn't use `git branchless bug-report`. 62 | placeholder: "Example: git version 2.28.0" 63 | 64 | -------------------------------------------------------------------------------- /git-branchless-lib/bin/testing/regression_test_cherry_pick.rs: -------------------------------------------------------------------------------- 1 | //! Test to make sure that `Repo::cherry_pick_fast` produces the same results as 2 | //! regular Git when applying a patch. 3 | 4 | use std::path::PathBuf; 5 | 6 | use branchless::git::{CherryPickFastOptions, MaybeZeroOid, Repo}; 7 | use eyre::Context; 8 | 9 | fn main() -> eyre::Result<()> { 10 | let path_to_repo = std::env::var("PATH_TO_REPO") 11 | .wrap_err("Could not read PATH_TO_REPO environment variable")?; 12 | let repo = Repo::from_dir(&PathBuf::from(path_to_repo))?; 13 | 14 | let mut next_commit = repo.find_commit_or_fail(repo.get_head_info()?.oid.unwrap())?; 15 | for i in 1..1000 { 16 | let current_commit = next_commit; 17 | next_commit = match current_commit.get_parents().first() { 18 | Some(parent_commit) => parent_commit.clone(), 19 | None => { 20 | println!("Reached root commit, exiting."); 21 | break; 22 | } 23 | }; 24 | println!("Test #{i}: {current_commit:?}"); 25 | 26 | let parent_commit = match current_commit.get_only_parent() { 27 | Some(parent_commit) => parent_commit, 28 | None => { 29 | println!( 30 | "Skipping since commit had multiple parents: {:?}", 31 | current_commit.get_parents(), 32 | ); 33 | continue; 34 | } 35 | }; 36 | 37 | let tree = repo.cherry_pick_fast( 38 | ¤t_commit, 39 | &parent_commit, 40 | &CherryPickFastOptions { 41 | reuse_parent_tree_if_possible: false, 42 | }, 43 | )?; 44 | 45 | let expected_tree_oid = current_commit.get_tree_oid(); 46 | if MaybeZeroOid::NonZero(tree.get_oid()) != expected_tree_oid { 47 | println!( 48 | "Trees are NOT equal, actual {actual} vs expected {expected}\n\ 49 | Try running: git diff-tree -p {expected} {actual}", 50 | expected = expected_tree_oid.to_string(), 51 | actual = tree.get_oid().to_string(), 52 | ); 53 | std::process::exit(1); 54 | } 55 | } 56 | 57 | Ok(()) 58 | } 59 | -------------------------------------------------------------------------------- /.github/workflows/linux-git-devel.yml: -------------------------------------------------------------------------------- 1 | name: "Linux (Git devel)" 2 | 3 | on: 4 | # TODO(#1416): re-enable once tests are passing on Git v2.46+ 5 | # schedule: 6 | # # Run once every day at 6:40AM UTC. 7 | # - cron: "40 6 * * *" 8 | 9 | # push: 10 | # branches: 11 | # - master 12 | 13 | # pull_request: 14 | # paths: 15 | # - ".github/workflows/*.yml" 16 | 17 | workflow_dispatch: 18 | 19 | env: 20 | CARGO_INCREMENTAL: 0 21 | RUST_BACKTRACE: short 22 | 23 | jobs: 24 | run-tests: 25 | if: startsWith(github.head_ref, 'ci-') || github.head_ref == '' 26 | runs-on: ubuntu-latest 27 | 28 | steps: 29 | - uses: actions/checkout@v5 30 | with: 31 | path: git-master 32 | repository: git/git 33 | ref: master 34 | 35 | - uses: actions/checkout@v5 36 | with: 37 | path: git-next 38 | repository: git/git 39 | ref: next 40 | 41 | - name: Install dependencies 42 | run: | 43 | sudo apt-get update --fix-missing 44 | # List of dependencies from https://git-scm.com/book/en/v2/Getting-Started-Installing-Git 45 | sudo apt-get install dh-autoreconf libcurl4-gnutls-dev libexpat1-dev gettext libz-dev libssl-dev 46 | 47 | - name: Build Git `master` 48 | run: (cd git-master && make) 49 | 50 | - name: Build Git `next` 51 | run: (cd git-next && make) 52 | 53 | - name: Set up Rust 54 | uses: actions-rs/toolchain@v1 55 | with: 56 | profile: minimal 57 | toolchain: 1.82 58 | override: true 59 | 60 | - uses: actions/checkout@v5 61 | with: 62 | path: git-branchless 63 | 64 | - name: Run Rust tests on Git `master` 65 | timeout-minutes: 30 66 | run: | 67 | export TEST_GIT="$PWD"/git-master/git 68 | export TEST_GIT_EXEC_PATH=$(dirname "$TEST_GIT") 69 | (cd git-branchless && cargo test --all-features --examples --tests --workspace --no-fail-fast) 70 | 71 | - name: Run Rust tests on Git `next` 72 | timeout-minutes: 30 73 | run: | 74 | export TEST_GIT="$PWD"/git-next/git 75 | export TEST_GIT_EXEC_PATH=$(dirname "$TEST_GIT") 76 | (cd git-branchless && cargo test --all-features --examples --tests --workspace --no-fail-fast) 77 | -------------------------------------------------------------------------------- /git-branchless-lib/tests/test_rewrite_evolve.rs: -------------------------------------------------------------------------------- 1 | use branchless::core::eventlog::{EventLogDb, EventReplayer}; 2 | use branchless::core::formatting::Glyphs; 3 | use branchless::core::rewrite::find_rewrite_target; 4 | use branchless::git::MaybeZeroOid; 5 | use branchless::testing::{make_git, Git, GitRunOptions}; 6 | use branchless::{core::effects::Effects, git::NonZeroOid}; 7 | 8 | fn find_rewrite_target_helper( 9 | effects: &Effects, 10 | git: &Git, 11 | oid: NonZeroOid, 12 | ) -> eyre::Result> { 13 | let repo = git.get_repo()?; 14 | let conn = repo.get_db_conn()?; 15 | let event_log_db = EventLogDb::new(&conn)?; 16 | let event_replayer = EventReplayer::from_event_log_db(effects, &repo, &event_log_db)?; 17 | let event_cursor = event_replayer.make_default_cursor(); 18 | 19 | let rewrite_target = find_rewrite_target(&event_replayer, event_cursor, oid); 20 | Ok(rewrite_target) 21 | } 22 | 23 | #[test] 24 | fn test_find_rewrite_target() -> eyre::Result<()> { 25 | let effects = Effects::new_suppress_for_test(Glyphs::text()); 26 | let git = make_git()?; 27 | 28 | git.init_repo()?; 29 | let commit_time = 1; 30 | let old_oid = git.commit_file("test1", commit_time)?; 31 | 32 | { 33 | git.run(&["commit", "--amend", "-m", "test1 amended once"])?; 34 | let new_oid: MaybeZeroOid = { 35 | let (stdout, _stderr) = git.run(&["rev-parse", "HEAD"])?; 36 | stdout.trim().parse()? 37 | }; 38 | let rewrite_target = find_rewrite_target_helper(&effects, &git, old_oid)?; 39 | assert_eq!(rewrite_target, Some(new_oid)); 40 | } 41 | 42 | { 43 | git.run(&["commit", "--amend", "-m", "test1 amended twice"])?; 44 | let new_oid: MaybeZeroOid = { 45 | let (stdout, _stderr) = git.run(&["rev-parse", "HEAD"])?; 46 | stdout.trim().parse()? 47 | }; 48 | let rewrite_target = find_rewrite_target_helper(&effects, &git, old_oid)?; 49 | assert_eq!(rewrite_target, Some(new_oid)); 50 | } 51 | 52 | { 53 | git.run_with_options( 54 | &["commit", "--amend", "-m", "create test1.txt"], 55 | &GitRunOptions { 56 | time: commit_time, 57 | ..Default::default() 58 | }, 59 | )?; 60 | let rewrite_target = find_rewrite_target_helper(&effects, &git, old_oid)?; 61 | assert_eq!(rewrite_target, None); 62 | } 63 | 64 | Ok(()) 65 | } 66 | -------------------------------------------------------------------------------- /demos/demo_helpers.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail 3 | BASE_DIR=$(realpath "$(dirname "$0")") 4 | 5 | UPLOAD=false 6 | PREVIEW=false 7 | DEBUG=false 8 | parse_args() { 9 | for arg in "$@"; do 10 | case "$arg" in 11 | -h|--help) 12 | echo 'Run a given demo. 13 | Arguments: 14 | --preview: Preview the asciicast. 15 | --upload: Upload to asciinema (after previewing, if necessary). 16 | --debug: Show the asciicast as it is being recorded. Note that what you see 17 | will not be exactly the same as what is recorded. 18 | ' 19 | exit 20 | ;; 21 | --upload) 22 | UPLOAD=true 23 | ;; 24 | --preview) 25 | PREVIEW=true 26 | ;; 27 | --debug) 28 | DEBUG=true 29 | ;; 30 | *) 31 | echo "Unrecognized argument: $arg" 32 | exit 1 33 | ;; 34 | esac 35 | done 36 | } 37 | 38 | set_up_git_repo() { 39 | local dirname 40 | dirname=$(mktemp -d) 41 | mkdir -p "$dirname" 42 | cd "$dirname" 43 | git init 44 | git branchless init 45 | alias git='git-branchless wrap' 46 | git commit -m 'Initial commit' --allow-empty 47 | trap "rm -rf '$dirname'" EXIT 48 | } 49 | 50 | confirm() { 51 | local message="$1" 52 | read -p "$message [yN] " choice 53 | case choice in 54 | y|Y) 55 | return 0 56 | ;; 57 | *) 58 | echo 'Cancelled.' 59 | return 1 60 | ;; 61 | esac 62 | } 63 | 64 | run_demo() { 65 | local expect_script=" 66 | source $BASE_DIR/demo_helpers.tcl 67 | spawn asciinema rec -c \"PS1='$ ' bash --norc\" 68 | expect_prompt 69 | 70 | $1 71 | 72 | quit_and_dump_asciicast_path 73 | " 74 | 75 | if [[ "$DEBUG" == true ]]; then 76 | echo "$expect_script" | /usr/bin/env expect 77 | return 78 | fi 79 | 80 | echo "Recording demo (terminal size is $(tput cols)x$(tput lines))..." 81 | if [[ "$PREVIEW" == 'false' ]]; then 82 | echo '(Pass --preview to play the demo automatically once done)' 83 | fi 84 | local asciicast_path 85 | asciicast_path=$(echo "$expect_script" | /usr/bin/env expect | tail -1) 86 | echo "$asciicast_path" 87 | 88 | if [[ "$PREVIEW" == 'true' ]]; then 89 | asciinema play "$asciicast_path" 90 | fi 91 | if [[ "$UPLOAD" == 'true' ]]; then 92 | if [[ "$PREVIEW" == 'true' ]] && ! confirm "Upload?"; then 93 | return 94 | fi 95 | : asciinema upload "$asciicast_path" 96 | fi 97 | } 98 | -------------------------------------------------------------------------------- /git-branchless-lib/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | authors = ["Waleed Khan "] 3 | description = "Support library for git-branchless." 4 | edition = "2021" 5 | keywords = ["git"] 6 | license = "MIT OR Apache-2.0" 7 | name = "git-branchless-lib" 8 | repository = "https://github.com/arxanas/git-branchless" 9 | rust-version = "1.82" 10 | version = "0.10.0" 11 | 12 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 13 | 14 | [lib] 15 | name = "branchless" 16 | 17 | [features] 18 | default = [] 19 | integration-test-bin = [] 20 | 21 | [[bench]] 22 | harness = false 23 | name = "benches" 24 | 25 | ## Testing binaries ## 26 | [[bin]] 27 | name = "git-branchless-regression-test-cherry-pick" 28 | path = "bin/testing/regression_test_cherry_pick.rs" 29 | required-features = ["integration-test-bin"] 30 | test = true 31 | 32 | [[bin]] 33 | name = "git-branchless-regression-test-record" 34 | path = "bin/testing/regression_test_record.rs" 35 | required-features = ["integration-test-bin"] 36 | test = true 37 | 38 | [[bin]] 39 | name = "git-branchless-profile-changed-paths" 40 | path = "bin/testing/profile_changed_paths.rs" 41 | required-features = ["integration-test-bin"] 42 | test = true 43 | 44 | [dependencies] 45 | anyhow = { workspace = true } 46 | assert_cmd = { workspace = true } 47 | async-trait = { workspace = true } 48 | bstr = { workspace = true } 49 | chashmap = { workspace = true } 50 | chrono = { workspace = true } 51 | color-eyre = { workspace = true } 52 | concolor = { workspace = true } 53 | console = { workspace = true } 54 | cursive = { workspace = true } 55 | eden_dag = { workspace = true } 56 | eyre = { workspace = true } 57 | futures = { workspace = true } 58 | git2 = { workspace = true } 59 | indicatif = { workspace = true } 60 | itertools = { workspace = true } 61 | lazy_static = { workspace = true } 62 | once_cell = { workspace = true } 63 | portable-pty = { workspace = true } 64 | rayon = { workspace = true } 65 | regex = { workspace = true } 66 | rusqlite = { workspace = true } 67 | scm-record = { workspace = true } 68 | serde = { workspace = true, features = ["derive"] } 69 | shell-words = { workspace = true } 70 | tempfile = { workspace = true } 71 | textwrap = { workspace = true } 72 | thiserror = { workspace = true } 73 | tracing = { workspace = true } 74 | tracing-chrome = { workspace = true } 75 | tracing-error = { workspace = true } 76 | tracing-subscriber = { workspace = true } 77 | vt100 = { workspace = true } 78 | 79 | [dev-dependencies] 80 | cc = { workspace = true } 81 | criterion = { workspace = true } 82 | insta = { workspace = true } 83 | -------------------------------------------------------------------------------- /git-branchless/tests/test_repair.rs: -------------------------------------------------------------------------------- 1 | use lib::git::{BranchType, ReferenceName}; 2 | use lib::testing::make_git; 3 | 4 | #[test] 5 | fn test_repair_broken_commit() -> eyre::Result<()> { 6 | let git = make_git()?; 7 | git.init_repo()?; 8 | 9 | git.detach_head()?; 10 | git.commit_file("test1", 1)?; 11 | git.commit_file("test2", 2)?; 12 | let test3_oid = git.commit_file("test3", 3)?; 13 | git.run(&["checkout", "HEAD^"])?; 14 | 15 | let repo = git.get_repo()?; 16 | repo.find_reference(&ReferenceName::from(format!("refs/branchless/{test3_oid}")))? 17 | .unwrap() 18 | .delete()?; 19 | git.run(&["gc", "--prune=now"])?; 20 | 21 | { 22 | let stdout = git.smartlog()?; 23 | insta::assert_snapshot!(stdout, @r###" 24 | O f777ecc (master) create initial.txt 25 | | 26 | o 62fc20d create test1.txt 27 | | 28 | @ 96d1c37 create test2.txt 29 | | 30 | o 70deb1e 31 | "###); 32 | } 33 | 34 | { 35 | let (stdout, _stderr) = git.branchless("repair", &["--no-dry-run"])?; 36 | insta::assert_snapshot!(stdout, @"Found and repaired 1 broken commit: 70deb1e28791d8e7dd5a1f0c871a51b91282562f 37 | "); 38 | } 39 | 40 | { 41 | let stdout = git.smartlog()?; 42 | insta::assert_snapshot!(stdout, @r###" 43 | O f777ecc (master) create initial.txt 44 | | 45 | o 62fc20d create test1.txt 46 | | 47 | @ 96d1c37 create test2.txt 48 | "###); 49 | } 50 | 51 | Ok(()) 52 | } 53 | 54 | #[test] 55 | fn test_repair_broken_branch() -> eyre::Result<()> { 56 | let git = make_git()?; 57 | 58 | if !git.supports_reference_transactions()? { 59 | return Ok(()); 60 | } 61 | git.init_repo()?; 62 | 63 | git.commit_file("test1", 1)?; 64 | git.run(&["branch", "foo"])?; 65 | 66 | let repo = git.get_repo()?; 67 | repo.find_branch("foo", BranchType::Local)? 68 | .unwrap() 69 | .into_reference() 70 | .delete()?; 71 | 72 | { 73 | let (stdout, _stderr) = git.branchless("repair", &["--no-dry-run"])?; 74 | insta::assert_snapshot!(stdout, @"Found and repaired 1 broken branch: foo 75 | "); 76 | } 77 | 78 | { 79 | // Advance the event cursor so that we can write `--event-id=-1` below. 80 | git.commit_file("test2", 2)?; 81 | 82 | let (stdout, _stderr) = git.branchless("smartlog", &["--event-id=-1"])?; 83 | insta::assert_snapshot!(stdout, @r###" 84 | : 85 | @ 96d1c37 (> master) create test2.txt 86 | "###); 87 | } 88 | 89 | Ok(()) 90 | } 91 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "git-branchless"; 3 | 4 | outputs = { self, nixpkgs, ... }: 5 | let 6 | lib = nixpkgs.lib; 7 | systems = [ 8 | "aarch64-linux" 9 | "aarch64-darwin" 10 | "i686-linux" 11 | "x86_64-darwin" 12 | "x86_64-linux" 13 | ]; 14 | foreachSystem = lib.genAttrs systems; 15 | in 16 | { 17 | overlays.default = (final: prev: { 18 | 19 | # reuse the definition from nixpkgs git-branchless 20 | git-branchless = prev.git-branchless.overrideAttrs ({ meta, ... }: { 21 | name = "git-branchless"; 22 | src = self; 23 | cargoDeps = final.rustPlatform.importCargoLock { 24 | lockFile = ./Cargo.lock; 25 | }; 26 | 27 | # for `flake.nix` contributors: put additional local overrides here. 28 | # if the changes are also applicable to the `git-branchless` package 29 | # in nixpkgs, consider first improving the definition there, and then 30 | # update the `flake.lock` here. 31 | 32 | # in case local overrides might confuse upstream maintainers, 33 | # we do not list them here: 34 | meta = (removeAttrs meta [ "maintainers" ]) // { 35 | # to correctly generate meta.position for back trace: 36 | inherit (meta) description; 37 | }; 38 | }); 39 | 40 | # reuse the definition for git-branchless 41 | scm-diff-editor = final.git-branchless.overrideAttrs (finalAttrs: prevAttrs: { 42 | name = "scm-diff-editor"; 43 | meta = prevAttrs.meta // { 44 | mainProgram = finalAttrs.name; 45 | description = "UI to interactively select changes, bundled in git-branchless"; 46 | }; 47 | 48 | buildAndTestSubdir = "scm-record"; 49 | buildFeatures = [ "scm-diff-editor" ]; 50 | 51 | # remove the git-branchless specific build commands 52 | postInstall = ""; 53 | preCheck = ""; 54 | checkFlags = ""; 55 | }); 56 | }); 57 | 58 | packages = foreachSystem (system: 59 | let 60 | pkgs = nixpkgs.legacyPackages.${system}.extend self.overlays.default; 61 | in 62 | { 63 | inherit (pkgs) 64 | git-branchless scm-diff-editor; 65 | default = pkgs.git-branchless; 66 | } 67 | ); 68 | 69 | checks = foreachSystem (system: { 70 | git-branchless = 71 | self.packages.${system}.git-branchless.overrideAttrs ({ preCheck, ... }: { 72 | cargoBuildType = "debug"; 73 | cargoCheckType = "debug"; 74 | preCheck = '' 75 | export RUST_BACKTRACE=1 76 | '' + preCheck; 77 | }); 78 | }); 79 | }; 80 | } 81 | -------------------------------------------------------------------------------- /git-branchless/src/commands/wrap.rs: -------------------------------------------------------------------------------- 1 | //! Wrap a user-provided Git command, so that `git-branchless` can do special 2 | //! processing. 3 | 4 | use std::process::Command; 5 | use std::time::SystemTime; 6 | 7 | use eyre::Context; 8 | use itertools::Itertools; 9 | 10 | use lib::core::eventlog::{EventLogDb, EventTransactionId, BRANCHLESS_TRANSACTION_ID_ENV_VAR}; 11 | use lib::git::{GitRunInfo, Repo}; 12 | use lib::util::{ExitCode, EyreExitOr}; 13 | 14 | fn pass_through_git_command_inner( 15 | git_run_info: &GitRunInfo, 16 | args: &[&str], 17 | event_tx_id: Option, 18 | ) -> EyreExitOr<()> { 19 | let GitRunInfo { 20 | path_to_git, 21 | working_directory, 22 | env, 23 | } = git_run_info; 24 | let mut command = Command::new(path_to_git); 25 | command.current_dir(working_directory); 26 | command.args(args); 27 | command.env_clear(); 28 | command.envs(env.iter()); 29 | if let Some(event_tx_id) = event_tx_id { 30 | command.env(BRANCHLESS_TRANSACTION_ID_ENV_VAR, event_tx_id.to_string()); 31 | } 32 | let exit_status = command.status().wrap_err("Running Git command")?; 33 | let exit_code: isize = exit_status.code().unwrap_or(1).try_into()?; 34 | let exit_code = ExitCode(exit_code); 35 | if exit_code.is_success() { 36 | Ok(Ok(())) 37 | } else { 38 | Ok(Err(exit_code)) 39 | } 40 | } 41 | 42 | fn pass_through_git_command + std::fmt::Debug>( 43 | git_run_info: &GitRunInfo, 44 | args: &[S], 45 | event_tx_id: Option, 46 | ) -> EyreExitOr<()> { 47 | pass_through_git_command_inner( 48 | git_run_info, 49 | args.iter().map(AsRef::as_ref).collect_vec().as_slice(), 50 | event_tx_id, 51 | ) 52 | } 53 | 54 | fn make_event_tx_id + std::fmt::Debug>( 55 | args: &[S], 56 | ) -> eyre::Result { 57 | let now = SystemTime::now(); 58 | let repo = Repo::from_current_dir()?; 59 | let conn = repo.get_db_conn()?; 60 | let event_log_db = EventLogDb::new(&conn)?; 61 | let event_tx_id = { 62 | let message = args.first().map(|s| s.as_ref()).unwrap_or("wrap"); 63 | event_log_db.make_transaction_id(now, message)? 64 | }; 65 | Ok(event_tx_id) 66 | } 67 | 68 | /// Run the provided Git command, but wrapped in an event transaction. 69 | pub fn wrap + std::fmt::Debug>( 70 | git_run_info: &GitRunInfo, 71 | args: &[S], 72 | ) -> EyreExitOr<()> { 73 | // We may not be able to make an event transaction ID (such as if there is 74 | // no repository in the current directory). Ignore the error in that case. 75 | let event_tx_id = make_event_tx_id(args).ok(); 76 | 77 | let exit_code = pass_through_git_command(git_run_info, args, event_tx_id)?; 78 | Ok(exit_code) 79 | } 80 | -------------------------------------------------------------------------------- /git-branchless-lib/src/core/rewrite/mod.rs: -------------------------------------------------------------------------------- 1 | //! Tools for editing the commit graph. 2 | 3 | mod evolve; 4 | mod execute; 5 | mod plan; 6 | pub mod rewrite_hooks; 7 | 8 | use std::sync::Mutex; 9 | 10 | pub use evolve::{find_abandoned_children, find_rewrite_target}; 11 | pub use execute::{ 12 | execute_rebase_plan, move_branches, ExecuteRebasePlanOptions, ExecuteRebasePlanResult, 13 | FailedMergeInfo, MergeConflictRemediation, 14 | }; 15 | pub use plan::{ 16 | BuildRebasePlanError, BuildRebasePlanOptions, OidOrLabel, RebaseCommand, RebasePlan, 17 | RebasePlanBuilder, RebasePlanPermissions, 18 | }; 19 | use tracing::instrument; 20 | 21 | use crate::core::task::{Resource, ResourcePool}; 22 | use crate::git::Repo; 23 | 24 | /// A thread-safe [`Repo`] resource pool. 25 | #[derive(Debug)] 26 | pub struct RepoResource { 27 | repo: Mutex, 28 | } 29 | 30 | impl RepoResource { 31 | /// Make a copy of the provided [`Repo`] and use that to populate the 32 | /// [`ResourcePool`]. 33 | #[instrument] 34 | pub fn new_pool(repo: &Repo) -> eyre::Result> { 35 | let repo = Mutex::new(repo.try_clone()?); 36 | let resource = Self { repo }; 37 | Ok(ResourcePool::new(resource)) 38 | } 39 | } 40 | 41 | impl Resource for RepoResource { 42 | type Output = Repo; 43 | 44 | type Error = eyre::Error; 45 | 46 | fn try_create(&self) -> Result { 47 | let repo = self 48 | .repo 49 | .lock() 50 | .map_err(|_| eyre::eyre!("Poisoned mutex for RepoResource"))?; 51 | let repo = repo.try_clone()?; 52 | Ok(repo) 53 | } 54 | } 55 | 56 | /// Type synonym for [`ResourcePool`]. 57 | pub type RepoPool = ResourcePool; 58 | 59 | /// Testing helpers. 60 | pub mod testing { 61 | use std::collections::HashSet; 62 | use std::path::PathBuf; 63 | 64 | use chashmap::CHashMap; 65 | 66 | use crate::core::dag::Dag; 67 | use crate::core::rewrite::{BuildRebasePlanOptions, RebasePlanPermissions}; 68 | use crate::git::NonZeroOid; 69 | 70 | use super::RebasePlanBuilder; 71 | 72 | /// Create a `RebasePlanPermissions` that can rewrite any commit, for testing. 73 | pub fn omnipotent_rebase_plan_permissions( 74 | dag: &Dag, 75 | build_options: BuildRebasePlanOptions, 76 | ) -> eyre::Result { 77 | Ok(RebasePlanPermissions { 78 | build_options, 79 | allowed_commits: dag.query_all()?, 80 | }) 81 | } 82 | 83 | /// Get the internal touched paths cache for a `RebasePlanBuilder`. 84 | pub fn get_builder_touched_paths_cache<'a>( 85 | builder: &'a RebasePlanBuilder, 86 | ) -> &'a CHashMap> { 87 | &builder.touched_paths_cache 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /git-branchless/src/commands/snapshot.rs: -------------------------------------------------------------------------------- 1 | //! Manage working copy snapshots. These commands are primarily intended for 2 | //! testing and debugging. 3 | 4 | use std::fmt::Write; 5 | use std::time::SystemTime; 6 | 7 | use cursive_core::theme::BaseColor; 8 | use cursive_core::utils::markup::StyledString; 9 | use eyre::Context; 10 | use lib::core::check_out::{create_snapshot, restore_snapshot}; 11 | use lib::core::effects::Effects; 12 | use lib::core::eventlog::EventLogDb; 13 | use lib::git::{GitRunInfo, GitRunResult, NonZeroOid, Repo, WorkingCopySnapshot}; 14 | use lib::util::{ExitCode, EyreExitOr}; 15 | 16 | pub fn create(effects: &Effects, git_run_info: &GitRunInfo) -> EyreExitOr<()> { 17 | let repo = Repo::from_dir(&git_run_info.working_directory)?; 18 | let conn = repo.get_db_conn()?; 19 | let event_log_db = EventLogDb::new(&conn)?; 20 | let event_tx_id = event_log_db.make_transaction_id(SystemTime::now(), "snapshot create")?; 21 | let snapshot = create_snapshot(effects, git_run_info, &repo, &event_log_db, event_tx_id)?; 22 | writeln!( 23 | effects.get_output_stream(), 24 | "{}", 25 | snapshot.base_commit.get_oid() 26 | )?; 27 | 28 | // Don't write `git reset` output to stdout. 29 | let GitRunResult { 30 | exit_code, 31 | stdout: _, 32 | stderr: _, 33 | } = git_run_info 34 | .run_silent( 35 | &repo, 36 | Some(event_tx_id), 37 | &["reset", "--hard", "HEAD", "--"], 38 | Default::default(), 39 | ) 40 | .wrap_err("Discarding working copy")?; 41 | 42 | if exit_code.is_success() { 43 | Ok(Ok(())) 44 | } else { 45 | writeln!( 46 | effects.get_output_stream(), 47 | "{}", 48 | effects.get_glyphs().render(StyledString::styled( 49 | "Failed to clean up working copy state".to_string(), 50 | BaseColor::Red.light() 51 | ))? 52 | )?; 53 | Ok(Err(exit_code)) 54 | } 55 | } 56 | 57 | pub fn restore( 58 | effects: &Effects, 59 | git_run_info: &GitRunInfo, 60 | snapshot_oid: NonZeroOid, 61 | ) -> EyreExitOr<()> { 62 | let repo = Repo::from_dir(&git_run_info.working_directory)?; 63 | let conn = repo.get_db_conn()?; 64 | let event_log_db = EventLogDb::new(&conn)?; 65 | let event_tx_id = event_log_db.make_transaction_id(SystemTime::now(), "snapshot restore")?; 66 | 67 | let base_commit = repo.find_commit_or_fail(snapshot_oid)?; 68 | let snapshot = match WorkingCopySnapshot::try_from_base_commit(&repo, &base_commit)? { 69 | Some(snapshot) => snapshot, 70 | None => { 71 | writeln!( 72 | effects.get_error_stream(), 73 | "Not a snapshot commit: {snapshot_oid}" 74 | )?; 75 | return Ok(Err(ExitCode(1))); 76 | } 77 | }; 78 | 79 | restore_snapshot(effects, git_run_info, &repo, event_tx_id, &snapshot) 80 | } 81 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | # Originally from 2 | # https://github.com/martinvonz/jj/blob/028e6106f52978fdd120037d4b3db2f088cafcfa/.github/workflows/release.yml 3 | name: Build binaries for GitHub release 4 | 5 | on: 6 | release: 7 | types: [created, edited, published] 8 | 9 | permissions: read-all 10 | 11 | jobs: 12 | build-release: 13 | name: build-release 14 | permissions: 15 | contents: write 16 | strategy: 17 | fail-fast: false 18 | matrix: 19 | build: [linux-musl-x64, linux-musl-arm64, macos-x64, macos-arm64, win-msvc] 20 | include: 21 | - build: linux-musl-x64 22 | os: ubuntu-24.04 23 | target: x86_64-unknown-linux-musl 24 | - build: linux-musl-arm64 25 | os: ubuntu-24.04-arm 26 | target: aarch64-unknown-linux-musl 27 | - build: macos-x64 28 | os: macos-13 29 | target: x86_64-apple-darwin 30 | - build: macos-arm64 31 | os: macos-15 32 | target: aarch64-apple-darwin 33 | - build: win-msvc 34 | os: windows-2022 35 | target: x86_64-pc-windows-msvc 36 | runs-on: ${{ matrix.os }} 37 | 38 | steps: 39 | - name: Checkout repository 40 | uses: actions/checkout@v5 41 | 42 | - name: Install packages (Ubuntu) 43 | if: startsWith(matrix.os, 'ubuntu-24.04') 44 | run: | 45 | sudo apt-get update 46 | sudo apt-get install -y --no-install-recommends xz-utils liblz4-tool musl-tools 47 | 48 | - name: Install Rust 49 | uses: actions-rs/toolchain@16499b5e05bf2e26879000db0c1d13f7e13fa3af 50 | with: 51 | toolchain: stable 52 | profile: minimal 53 | override: true 54 | target: ${{ matrix.target }} 55 | 56 | - name: Build release binary 57 | uses: actions-rs/cargo@844f36862e911db73fe0815f00a4a2602c279505 58 | with: 59 | command: build 60 | args: --target ${{ matrix.target }} --verbose --release 61 | 62 | - name: Build archive 63 | shell: bash 64 | run: | 65 | outdir="./target/${{ matrix.target }}/release" 66 | staging="git-branchless-${{ github.event.release.tag_name }}-${{ matrix.target }}" 67 | mkdir -p "$staging" 68 | cp README.md LICENSE-{APACHE,MIT} "$staging/" 69 | if [ "${{ matrix.os }}" = "windows-2022" ]; then 70 | cp "target/${{ matrix.target }}/release/git-branchless.exe" "$staging/" 71 | cd "$staging" 72 | 7z a "../$staging.zip" . 73 | echo "ASSET=$staging.zip" >> $GITHUB_ENV 74 | else 75 | cp "target/${{ matrix.target }}/release/git-branchless" "$staging/" 76 | tar czf "$staging.tar.gz" -C "$staging" . 77 | echo "ASSET=$staging.tar.gz" >> $GITHUB_ENV 78 | fi 79 | 80 | - name: Upload release archive 81 | uses: actions/upload-release-asset@e8f9f06c4b078e705bd2ea027f0926603fc9b4d5 82 | env: 83 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 84 | with: 85 | upload_url: ${{ github.event.release.upload_url }} 86 | asset_path: ${{ env.ASSET }} 87 | asset_name: ${{ env.ASSET }} 88 | asset_content_type: application/octet-stream 89 | -------------------------------------------------------------------------------- /git-branchless-undo/src/tui/cursive.rs: -------------------------------------------------------------------------------- 1 | //! Utilities to render an interactive text-based user interface. 2 | 3 | use cursive::backends::crossterm; 4 | use cursive_buffered_backend::BufferedBackend; 5 | use cursive_core::theme::{Color, PaletteColor}; 6 | use cursive_core::{Cursive, CursiveRunner}; 7 | 8 | use lib::core::effects::Effects; 9 | 10 | /// Create an instance of a `CursiveRunner`, and clean it up afterward. 11 | pub fn with_siv) -> eyre::Result>( 12 | effects: &Effects, 13 | f: F, 14 | ) -> eyre::Result { 15 | // Use crossterm to ensure that we support Windows. 16 | let backend = crossterm::Backend::init()?; 17 | let backend = BufferedBackend::new(backend); 18 | 19 | let effects = effects.enable_tui_mode(); 20 | let mut siv = Cursive::new().into_runner(Box::new(backend)); 21 | siv.update_theme(|theme| { 22 | theme.shadow = false; 23 | theme.palette.extend(vec![ 24 | (PaletteColor::Background, Color::TerminalDefault), 25 | (PaletteColor::View, Color::TerminalDefault), 26 | (PaletteColor::Primary, Color::TerminalDefault), 27 | (PaletteColor::TitlePrimary, Color::TerminalDefault), 28 | (PaletteColor::TitleSecondary, Color::TerminalDefault), 29 | ]); 30 | }); 31 | f(effects, siv) 32 | } 33 | 34 | /// Type-safe "singleton" view: a kind of view which is addressed by name, for 35 | /// which exactly one copy exists in the Cursive application. 36 | pub trait SingletonView { 37 | /// Look up the instance of the singleton view in the application. Panics if 38 | /// it hasn't been added. 39 | fn find(siv: &mut Cursive) -> cursive_core::views::ViewRef; 40 | } 41 | 42 | /// Create a set of views with unique names. 43 | /// 44 | /// ``` 45 | /// # use cursive_core::Cursive; 46 | /// # use cursive_core::views::{EditView, TextView}; 47 | /// # use git_branchless_undo::declare_views; 48 | /// # use git_branchless_undo::tui::SingletonView; 49 | /// # fn main() { 50 | /// declare_views! { 51 | /// SomeDisplayView => TextView, 52 | /// SomeDataEntryView => EditView, 53 | /// } 54 | /// let mut siv = Cursive::new(); 55 | /// siv.add_layer::(TextView::new("Hello, world!").into()); 56 | /// assert_eq!(SomeDisplayView::find(&mut siv).get_content().source(), "Hello, world!"); 57 | /// # } 58 | /// ``` 59 | #[macro_export] 60 | macro_rules! declare_views { 61 | { $( $k:ident => $v:ty ),* $(,)? } => { 62 | $( 63 | struct $k { 64 | view: cursive_core::views::NamedView<$v>, 65 | } 66 | 67 | impl $crate::tui::SingletonView<$v> for $k { 68 | fn find(siv: &mut Cursive) -> cursive_core::views::ViewRef<$v> { 69 | siv.find_name::<$v>(stringify!($k)).unwrap() 70 | } 71 | } 72 | 73 | impl From<$v> for $k { 74 | fn from(view: $v) -> Self { 75 | use cursive_core::view::Nameable; 76 | let view = view.with_name(stringify!($k)); 77 | $k { view } 78 | } 79 | } 80 | 81 | impl cursive_core::view::ViewWrapper for $k { 82 | cursive_core::wrap_impl!(self.view: cursive_core::views::NamedView<$v>); 83 | } 84 | )* 85 | }; 86 | } 87 | -------------------------------------------------------------------------------- /git-branchless-lib/src/core/rewrite/evolve.rs: -------------------------------------------------------------------------------- 1 | use tracing::instrument; 2 | 3 | use crate::core::dag::{CommitSet, Dag}; 4 | use crate::core::eventlog::{Event, EventCursor, EventReplayer}; 5 | use crate::git::{MaybeZeroOid, NonZeroOid}; 6 | 7 | /// For a rewritten commit, find the newest version of the commit. 8 | /// 9 | /// For example, if we amend commit `abc` into commit `def1`, and then amend 10 | /// `def1` into `def2`, then we can traverse the event log to find out that `def2` 11 | /// is the newest version of `abc`. 12 | /// 13 | /// If a commit was rewritten into itself through some chain of events, then 14 | /// returns `None`, rather than the same commit OID. 15 | #[instrument] 16 | pub fn find_rewrite_target( 17 | event_replayer: &EventReplayer, 18 | event_cursor: EventCursor, 19 | oid: NonZeroOid, 20 | ) -> Option { 21 | let event = event_replayer.get_cursor_commit_latest_event(event_cursor, oid)?; 22 | match event { 23 | Event::RewriteEvent { 24 | timestamp: _, 25 | event_tx_id: _, 26 | old_commit_oid: MaybeZeroOid::NonZero(old_commit_oid), 27 | new_commit_oid, 28 | } => { 29 | if *old_commit_oid == oid && *new_commit_oid != MaybeZeroOid::NonZero(oid) { 30 | match new_commit_oid { 31 | MaybeZeroOid::Zero => Some(MaybeZeroOid::Zero), 32 | MaybeZeroOid::NonZero(new_commit_oid) => { 33 | let possible_newer_oid = 34 | find_rewrite_target(event_replayer, event_cursor, *new_commit_oid); 35 | match possible_newer_oid { 36 | Some(newer_commit_oid) => Some(newer_commit_oid), 37 | None => Some(MaybeZeroOid::NonZero(*new_commit_oid)), 38 | } 39 | } 40 | } 41 | } else { 42 | None 43 | } 44 | } 45 | 46 | Event::RewriteEvent { 47 | timestamp: _, 48 | event_tx_id: _, 49 | old_commit_oid: MaybeZeroOid::Zero, 50 | new_commit_oid: _, 51 | } 52 | | Event::RefUpdateEvent { .. } 53 | | Event::CommitEvent { .. } 54 | | Event::ObsoleteEvent { .. } 55 | | Event::UnobsoleteEvent { .. } 56 | | Event::WorkingCopySnapshot { .. } => None, 57 | } 58 | } 59 | 60 | /// Find commits which have been "abandoned" in the commit graph. 61 | /// 62 | /// A commit is considered "abandoned" if it's not obsolete, but one of its 63 | /// parents is. 64 | #[instrument] 65 | pub fn find_abandoned_children( 66 | dag: &Dag, 67 | event_replayer: &EventReplayer, 68 | event_cursor: EventCursor, 69 | oid: NonZeroOid, 70 | ) -> eyre::Result)>> { 71 | let rewritten_oid = match find_rewrite_target(event_replayer, event_cursor, oid) { 72 | Some(MaybeZeroOid::NonZero(rewritten_oid)) => rewritten_oid, 73 | Some(MaybeZeroOid::Zero) => oid, 74 | None => return Ok(None), 75 | }; 76 | let children = dag.query_children(CommitSet::from(oid))?; 77 | let children = dag.filter_visible_commits(children)?; 78 | let non_obsolete_children = children.difference(&dag.query_obsolete_commits()); 79 | let non_obsolete_children_oids = dag.commit_set_to_vec(&non_obsolete_children)?; 80 | 81 | Ok(Some((rewritten_oid, non_obsolete_children_oids))) 82 | } 83 | -------------------------------------------------------------------------------- /git-branchless/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | authors = ["Waleed Khan "] 3 | categories = ["command-line-utilities", "development-tools"] 4 | default-run = "git-branchless" 5 | description = "Branchless workflow for Git" 6 | documentation = "https://github.com/arxanas/git-branchless/wiki" 7 | edition = "2021" 8 | homepage = "https://github.com/arxanas/git-branchless" 9 | keywords = ["cli", "git"] 10 | license = "MIT OR Apache-2.0" 11 | name = "git-branchless" 12 | readme = "../README.md" 13 | repository = "https://github.com/arxanas/git-branchless" 14 | rust-version = "1.82" 15 | version = "0.10.0" 16 | 17 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 18 | 19 | [dependencies] 20 | bstr = { workspace = true } 21 | bugreport = { workspace = true } 22 | color-eyre = { workspace = true } 23 | console = { workspace = true } 24 | cursive_core = { workspace = true } 25 | eden_dag = { workspace = true } 26 | eyre = { workspace = true } 27 | fslock = { workspace = true } 28 | git-branchless-hook = { workspace = true } 29 | git-branchless-init = { workspace = true } 30 | git-branchless-invoke = { workspace = true } 31 | git-branchless-move = { workspace = true } 32 | git-branchless-navigation = { workspace = true } 33 | git-branchless-opts = { workspace = true } 34 | git-branchless-query = { workspace = true } 35 | git-branchless-record = { workspace = true } 36 | git-branchless-revset = { workspace = true } 37 | git-branchless-reword = { workspace = true } 38 | git-branchless-smartlog = { workspace = true } 39 | git-branchless-submit = { workspace = true } 40 | git-branchless-test = { workspace = true } 41 | git-branchless-undo = { workspace = true } 42 | itertools = { workspace = true } 43 | lazy_static = { workspace = true } 44 | lib = { workspace = true } 45 | man = { workspace = true } 46 | num_cpus = { workspace = true } 47 | once_cell = { workspace = true } 48 | path-slash = { workspace = true } 49 | rayon = { workspace = true } 50 | regex = { workspace = true } 51 | rusqlite = { workspace = true } 52 | scm-diff-editor = { workspace = true } 53 | thiserror = { workspace = true } 54 | tracing = { workspace = true } 55 | tracing-chrome = { workspace = true } 56 | tracing-error = { workspace = true } 57 | tracing-subscriber = { workspace = true } 58 | 59 | [dev-dependencies] 60 | insta = { workspace = true } 61 | 62 | [features] 63 | man-pages = [] 64 | 65 | [package.metadata.release] 66 | pre-release-replacements = [ 67 | { file = "../CHANGELOG.md", search = "Unreleased", replace = "{{version}}", min = 1 }, 68 | { file = "../CHANGELOG.md", search = "ReleaseDate", replace = "{{date}}", min = 1 }, 69 | { file = "../CHANGELOG.md", search = "", replace = "\n## [Unreleased] - ReleaseDate\n", exactly = 1 }, 70 | ] 71 | 72 | [[test]] 73 | name = "test_amend" 74 | 75 | [[test]] 76 | name = "test_branchless" 77 | 78 | [[test]] 79 | name = "test_bug_report" 80 | 81 | [[test]] 82 | name = "test_eventlog" 83 | 84 | [[test]] 85 | name = "test_gc" 86 | 87 | [[test]] 88 | name = "test_hide" 89 | 90 | [[test]] 91 | name = "test_hooks" 92 | 93 | [[test]] 94 | name = "test_init" 95 | 96 | [[test]] 97 | name = "test_move" 98 | 99 | [[test]] 100 | name = "test_navigation" 101 | 102 | [[test]] 103 | name = "test_repair" 104 | 105 | [[test]] 106 | name = "test_restack" 107 | 108 | [[test]] 109 | name = "test_reword" 110 | 111 | [[test]] 112 | name = "test_snapshot" 113 | 114 | [[test]] 115 | name = "test_split" 116 | 117 | [[test]] 118 | name = "test_sync" 119 | 120 | [[test]] 121 | name = "test_undo" 122 | 123 | [[test]] 124 | name = "test_wrap" 125 | -------------------------------------------------------------------------------- /.github/workflows/linux.yml: -------------------------------------------------------------------------------- 1 | name: Linux 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | 9 | env: 10 | CARGO_INCREMENTAL: 0 11 | RUST_BACKTRACE: short 12 | 13 | jobs: 14 | build-git: 15 | runs-on: ubuntu-latest 16 | 17 | strategy: 18 | matrix: 19 | # Use a tag from https://github.com/git/git/tags 20 | # Make sure to update `git-version` in the `run-tests` step as well. 21 | git-version: ["v2.24.3", "v2.29.2", "v2.33.1", "v2.37.3"] 22 | 23 | steps: 24 | - uses: actions/checkout@v5 25 | with: 26 | repository: git/git 27 | ref: ${{ matrix.git-version }} 28 | 29 | - uses: actions/cache@v4 30 | id: cache-git-build 31 | with: 32 | key: ${{ runner.os }}-git-${{ matrix.git-version }} 33 | path: | 34 | git 35 | git-* 36 | 37 | - name: Build Git ${{ matrix.git-version }} 38 | if: steps.cache-git-build.outputs.cache-hit != 'true' 39 | run: | 40 | sudo apt-get update --fix-missing 41 | # List of dependencies from https://git-scm.com/book/en/v2/Getting-Started-Installing-Git 42 | sudo apt-get install dh-autoreconf libcurl4-gnutls-dev libexpat1-dev gettext libz-dev libssl-dev 43 | make 44 | 45 | - name: Package Git 46 | run: tar -czf git.tar.gz git git-* 47 | 48 | - name: "Upload artifact: git" 49 | uses: actions/upload-artifact@v4 50 | with: 51 | name: git-${{ matrix.git-version }} 52 | path: git.tar.gz 53 | if-no-files-found: error 54 | 55 | run-tests: 56 | runs-on: ubuntu-latest 57 | needs: build-git 58 | 59 | strategy: 60 | matrix: 61 | git-version: ["v2.24.3", "v2.29.2", "v2.33.1", "v2.37.3"] 62 | 63 | steps: 64 | - uses: actions/checkout@v5 65 | - name: "Download artifact: git" 66 | uses: actions/download-artifact@v5 67 | with: 68 | name: git-${{ matrix.git-version }} 69 | 70 | - name: "Unpack artifact: git" 71 | run: tar -xf git.tar.gz 72 | 73 | - name: Set up Rust 74 | uses: actions-rs/toolchain@v1 75 | with: 76 | profile: minimal 77 | toolchain: 1.82 78 | override: true 79 | 80 | - name: Cache dependencies 81 | uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 82 | 83 | - name: Compile (all features) 84 | run: cargo build --all-features --all-targets --workspace 85 | 86 | # Don't test benches. 87 | - name: Run Rust tests (all features) 88 | timeout-minutes: 30 89 | run: | 90 | export TEST_GIT="$PWD"/git 91 | export TEST_GIT_EXEC_PATH=$(dirname "$TEST_GIT") 92 | cargo test --all-features --examples --tests --workspace --no-fail-fast 93 | 94 | # Note that `--doc` can't be combined with other tests. 95 | - name: Run Rust doc-tests (all features) 96 | timeout-minutes: 30 97 | run: | 98 | export TEST_GIT="$PWD"/git 99 | export TEST_GIT_EXEC_PATH=$(dirname "$TEST_GIT") 100 | cargo test --all-features --doc --workspace --no-fail-fast 101 | 102 | - name: Compile (no features) 103 | run: cargo build --no-default-features --all-targets --workspace 104 | 105 | - name: Run Rust tests (no default features) 106 | timeout-minutes: 30 107 | run: | 108 | export TEST_GIT="$PWD"/git 109 | export TEST_GIT_EXEC_PATH=$(dirname "$TEST_GIT") 110 | cargo test --no-default-features --examples --tests --workspace --no-fail-fast 111 | -------------------------------------------------------------------------------- /git-branchless-lib/src/util.rs: -------------------------------------------------------------------------------- 1 | //! Utility functions. 2 | 3 | use std::num::TryFromIntError; 4 | use std::path::PathBuf; 5 | use std::process::ExitStatus; 6 | 7 | /// Represents the code to exit the process with. 8 | #[must_use] 9 | #[derive(Copy, Clone, Debug)] 10 | pub struct ExitCode(pub isize); 11 | 12 | impl ExitCode { 13 | /// Return an exit code corresponding to success. 14 | pub fn success() -> Self { 15 | Self(0) 16 | } 17 | 18 | /// Determine whether or not this exit code represents a successful 19 | /// termination. 20 | pub fn is_success(&self) -> bool { 21 | match self { 22 | ExitCode(0) => true, 23 | ExitCode(_) => false, 24 | } 25 | } 26 | } 27 | 28 | impl TryFrom for ExitCode { 29 | type Error = TryFromIntError; 30 | 31 | fn try_from(status: ExitStatus) -> Result { 32 | let exit_code = status.code().unwrap_or(1); 33 | Ok(Self(exit_code.try_into()?)) 34 | } 35 | } 36 | 37 | /// Encapsulate both an `eyre::Result` and a possible subcommand exit code. 38 | /// 39 | /// Helper type alias for the common case that we want to run a computation and 40 | /// return `eyre::Result`, but it's also possible that we run a subcommand 41 | /// which returns an exit code that we want to propagate. See also `try_exit_code`. 42 | pub type EyreExitOr = eyre::Result>; 43 | 44 | /// Macro to propagate `ExitCode`s in the same way as the `try!` macro/the `?` 45 | /// operator. 46 | /// 47 | /// Ideally, we would make `ExitCode` implement `std::ops::Try`, but that's a 48 | /// nightly API. We could also make `ExitCode` implement `Error`, but this 49 | /// interacts badly with `eyre::Result`, because all `Error`s are convertible to 50 | /// `eyre::Error`, so our exit codes get treated at the same as other errors. 51 | /// So, instead, we have this macro to accomplish the same thing, but for 52 | /// `Result`s specifically. 53 | #[macro_export] 54 | macro_rules! try_exit_code { 55 | ($e:expr) => { 56 | match $e { 57 | Ok(value) => value, 58 | Err(exit_code) => { 59 | return Ok(Err(exit_code)); 60 | } 61 | } 62 | }; 63 | } 64 | 65 | /// Returns a path for a given file, searching through PATH to find it. 66 | pub fn get_from_path(exe_name: &str) -> Option { 67 | std::env::var_os("PATH").and_then(|paths| { 68 | std::env::split_paths(&paths).find_map(|dir| { 69 | let bash_path = dir.join(exe_name); 70 | if bash_path.is_file() { 71 | Some(bash_path) 72 | } else { 73 | None 74 | } 75 | }) 76 | }) 77 | } 78 | 79 | /// Returns the path to a shell suitable for running hooks. 80 | pub fn get_sh() -> Option { 81 | let exe_name = if cfg!(target_os = "windows") { 82 | "bash.exe" 83 | } else { 84 | "sh" 85 | }; 86 | // If we are on Windows, first look for git.exe, and try to use it's bash, otherwise it won't 87 | // be able to find git-branchless correctly. 88 | if cfg!(target_os = "windows") { 89 | // Git is typically installed at C:\Program Files\Git\cmd\git.exe with the cmd\ directory 90 | // in the path, however git-bash is usually not in PATH and is in bin\ directory: 91 | let git_path = get_from_path("git.exe").expect("Couldn't find git.exe"); 92 | let git_dir = git_path.parent().unwrap().parent().unwrap(); 93 | let git_bash = git_dir.join("bin").join(exe_name); 94 | if git_bash.is_file() { 95 | return Some(git_bash); 96 | } 97 | } 98 | get_from_path(exe_name) 99 | } 100 | -------------------------------------------------------------------------------- /git-branchless-lib/tests/test_git_run.rs: -------------------------------------------------------------------------------- 1 | use branchless::git::{GitRunInfo, GitRunOpts}; 2 | use branchless::testing::make_git; 3 | 4 | #[test] 5 | fn test_hook_working_dir() -> eyre::Result<()> { 6 | let git = make_git()?; 7 | 8 | if !git.supports_reference_transactions()? { 9 | return Ok(()); 10 | } 11 | 12 | git.init_repo()?; 13 | git.commit_file("test1", 1)?; 14 | 15 | std::fs::write( 16 | git.repo_path 17 | .join(".git") 18 | .join("hooks") 19 | .join("post-rewrite"), 20 | r#"#!/bin/sh 21 | # This won't work unless we're running the hook in the Git working copy. 22 | echo "Check if test1.txt exists" 23 | [ -f test1.txt ] && echo "test1.txt exists" 24 | "#, 25 | )?; 26 | 27 | { 28 | // Trigger the `post-rewrite` hook that we wrote above. 29 | let (stdout, stderr) = git.run(&["commit", "--amend", "-m", "foo"])?; 30 | insta::assert_snapshot!(stderr, @r###" 31 | branchless: processing 2 updates: branch master, ref HEAD 32 | branchless: processed commit: f23bf8f foo 33 | Check if test1.txt exists 34 | test1.txt exists 35 | "###); 36 | insta::assert_snapshot!(stdout, @r###" 37 | [master f23bf8f] foo 38 | Date: Thu Oct 29 12:34:56 2020 -0100 39 | 1 file changed, 1 insertion(+) 40 | create mode 100644 test1.txt 41 | "###); 42 | } 43 | 44 | Ok(()) 45 | } 46 | 47 | #[test] 48 | fn test_run_silent_failures() -> eyre::Result<()> { 49 | let git = make_git()?; 50 | git.init_repo()?; 51 | 52 | let git_run_info = GitRunInfo { 53 | path_to_git: git.path_to_git.clone(), 54 | working_directory: git.repo_path.clone(), 55 | env: Default::default(), 56 | }; 57 | 58 | let result = git_run_info.run_silent( 59 | &git.get_repo()?, 60 | None, 61 | &["some-nonexistent-command"], 62 | GitRunOpts { 63 | treat_git_failure_as_error: true, 64 | stdin: None, 65 | }, 66 | ); 67 | assert!(result.is_err()); 68 | 69 | let result = git_run_info.run_silent( 70 | &git.get_repo()?, 71 | None, 72 | &["some-nonexistent-command"], 73 | GitRunOpts { 74 | treat_git_failure_as_error: false, 75 | stdin: None, 76 | }, 77 | ); 78 | assert!(result.is_ok()); 79 | 80 | Ok(()) 81 | } 82 | 83 | // Creating symlinks on Windows may fail without administrator or developer 84 | // privileges, so this test is Unix only. See 85 | // https://doc.rust-lang.org/std/os/windows/fs/fn.symlink_dir.html#limitations 86 | // for more details. 87 | #[cfg(unix)] 88 | #[test] 89 | fn test_run_in_repo_tool_project() -> eyre::Result<()> { 90 | use std::{fs, os::unix}; 91 | 92 | let git = make_git()?; 93 | git.init_repo()?; 94 | 95 | let git_dir = git.repo_path.join(".git"); 96 | let repo_managed_dir = tempfile::tempdir()?; 97 | let repo_managed_git_dir = repo_managed_dir.path().join(".repo/test_repo"); 98 | fs::create_dir_all(&repo_managed_git_dir)?; 99 | fs::rename(&git_dir, &repo_managed_git_dir)?; 100 | 101 | unix::fs::symlink(repo_managed_git_dir, git_dir)?; 102 | 103 | let git_run_info = GitRunInfo { 104 | path_to_git: git.path_to_git.clone(), 105 | working_directory: git.repo_path.clone(), 106 | env: Default::default(), 107 | }; 108 | 109 | let result = git_run_info.run_silent( 110 | &git.get_repo()?, 111 | None, 112 | &["status"], 113 | GitRunOpts { 114 | treat_git_failure_as_error: true, 115 | stdin: None, 116 | }, 117 | ); 118 | assert!(result.is_ok()); 119 | 120 | Ok(()) 121 | } 122 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [profile.dev] 2 | # Disabling debug info speeds up builds a bunch, 3 | # and we don't rely on it for debugging that much. 4 | debug = 0 5 | 6 | [workspace] 7 | default-members = ["git-branchless"] 8 | members = [ 9 | "git-branchless-hook", 10 | "git-branchless-init", 11 | "git-branchless-invoke", 12 | "git-branchless-lib", 13 | "git-branchless-move", 14 | "git-branchless-navigation", 15 | "git-branchless-opts", 16 | "git-branchless-query", 17 | "git-branchless-record", 18 | "git-branchless-revset", 19 | "git-branchless-reword", 20 | "git-branchless-smartlog", 21 | "git-branchless-submit", 22 | "git-branchless-test", 23 | "git-branchless-undo", 24 | "git-branchless", 25 | "scm-bisect", 26 | ] 27 | resolver = "2" 28 | 29 | [workspace.metadata.release] 30 | consolidate-commits = true 31 | push = false 32 | tag = false 33 | 34 | [workspace.dependencies] 35 | anyhow = "1.0.99" 36 | async-trait = "0.1.89" 37 | bstr = "1.12.0" 38 | bugreport = "0.5.1" 39 | chashmap = "2.2.2" 40 | chrono = "0.4.41" 41 | chrono-english = "0.1.7" 42 | chronoutil = "0.2.7" 43 | clap = "4.5.46" 44 | clap_mangen = "0.2.27" 45 | color-eyre = "0.6.5" 46 | concolor = { version = "0.1.1", features = ["auto"] } 47 | console = "0.15.11" 48 | crossbeam = "0.8.4" 49 | cursive = { version = "0.20.0", default-features = false, features = [ 50 | "crossterm-backend", 51 | ] } 52 | cursive_buffered_backend = "0.6.2" 53 | cursive_core = "0.3.7" 54 | eden_dag = { package = "sapling-dag", version = "0.1.0" } 55 | eyre = "0.6.12" 56 | fslock = "0.2.1" 57 | futures = "0.3.30" 58 | git-branchless-hook = { version = "0.10.0", path = "git-branchless-hook" } 59 | git-branchless-init = { version = "0.10.0", path = "git-branchless-init" } 60 | git-branchless-invoke = { version = "0.10.0", path = "git-branchless-invoke" } 61 | git-branchless-move = { version = "0.10.0", path = "git-branchless-move" } 62 | git-branchless-navigation = { version = "0.10.0", path = "git-branchless-navigation" } 63 | git-branchless-opts = { version = "0.10.0", path = "git-branchless-opts" } 64 | git-branchless-query = { version = "0.10.0", path = "git-branchless-query" } 65 | git-branchless-record = { version = "0.10.0", path = "git-branchless-record" } 66 | git-branchless-revset = { version = "0.10.0", path = "git-branchless-revset" } 67 | git-branchless-reword = { version = "0.10.0", path = "git-branchless-reword" } 68 | git-branchless-smartlog = { version = "0.10.0", path = "git-branchless-smartlog" } 69 | git-branchless-submit = { version = "0.10.0", path = "git-branchless-submit" } 70 | git-branchless-test = { version = "0.10.0", path = "git-branchless-test" } 71 | git-branchless-undo = { version = "0.10.0", path = "git-branchless-undo" } 72 | git2 = { version = "0.20.0", default-features = false } 73 | glob = "0.3.3" 74 | indexmap = "2.11.0" 75 | indicatif = { version = "0.17.11", features = ["improved_unicode"] } 76 | itertools = "0.14.0" 77 | lalrpop = "0.19.12" 78 | lalrpop-util = "0.19.12" 79 | lazy_static = "1.5.0" 80 | lib = { package = "git-branchless-lib", version = "0.10.0", path = "git-branchless-lib" } 81 | man = "0.3.0" 82 | num_cpus = "1.17.0" 83 | once_cell = "1.21.3" 84 | path-slash = "0.2.1" 85 | portable-pty = "0.8.1" 86 | rayon = "1.11.0" 87 | regex = "1.11.0" 88 | rusqlite = { version = "0.29.0", features = ["bundled"] } 89 | scm-bisect = { version = "0.3.0", path = "scm-bisect" } 90 | scm-diff-editor = "0.4.0" 91 | scm-record = "0.5.0" 92 | serde = { version = "1.0.219", features = ["derive"] } 93 | serde_json = "1.0.143" 94 | shell-words = "1.1.0" 95 | skim = "0.10.4" 96 | tempfile = "3.21.0" 97 | textwrap = "0.16.2" 98 | thiserror = "2.0.12" 99 | tracing = "0.1.41" 100 | tracing-chrome = "0.6.0" 101 | tracing-error = "0.2.1" 102 | tracing-subscriber = { version = "=0.3.11", features = ["env-filter"] } 103 | vt100 = "0.15.2" 104 | 105 | # dev-dependencies 106 | assert_cmd = "2.0.17" 107 | cc = "1.2.37" 108 | criterion = { version = "0.5.1", features = ["html_reports"] } 109 | insta = "1.43.1" 110 | maplit = "1.0.2" 111 | -------------------------------------------------------------------------------- /git-branchless-query/src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Write; 2 | 3 | use git_branchless_invoke::CommandContext; 4 | use itertools::Itertools; 5 | use lib::core::dag::Dag; 6 | use lib::core::effects::{Effects, OperationType}; 7 | use lib::core::eventlog::{EventLogDb, EventReplayer}; 8 | use lib::core::repo_ext::RepoExt; 9 | use lib::git::{CategorizedReferenceName, GitRunInfo, Repo}; 10 | use lib::util::{ExitCode, EyreExitOr}; 11 | use tracing::instrument; 12 | 13 | use git_branchless_opts::{QueryArgs, ResolveRevsetOptions, Revset}; 14 | use git_branchless_revset::resolve_commits; 15 | 16 | /// `query` command. 17 | #[instrument] 18 | pub fn command_main(ctx: CommandContext, args: QueryArgs) -> EyreExitOr<()> { 19 | let CommandContext { 20 | effects, 21 | git_run_info, 22 | } = ctx; 23 | let QueryArgs { 24 | revset, 25 | resolve_revset_options, 26 | show_branches, 27 | raw, 28 | } = args; 29 | query( 30 | &effects, 31 | &git_run_info, 32 | revset, 33 | &resolve_revset_options, 34 | show_branches, 35 | raw, 36 | ) 37 | } 38 | 39 | #[instrument] 40 | fn query( 41 | effects: &Effects, 42 | git_run_info: &GitRunInfo, 43 | query: Revset, 44 | resolve_revset_options: &ResolveRevsetOptions, 45 | show_branches: bool, 46 | raw: bool, 47 | ) -> EyreExitOr<()> { 48 | let repo = Repo::from_current_dir()?; 49 | let conn = repo.get_db_conn()?; 50 | let event_log_db = EventLogDb::new(&conn)?; 51 | let event_replayer = EventReplayer::from_event_log_db(effects, &repo, &event_log_db)?; 52 | let event_cursor = event_replayer.make_default_cursor(); 53 | let references_snapshot = repo.get_references_snapshot()?; 54 | let mut dag = Dag::open_and_sync( 55 | effects, 56 | &repo, 57 | &event_replayer, 58 | event_cursor, 59 | &references_snapshot, 60 | )?; 61 | 62 | let commit_set = 63 | match resolve_commits(effects, &repo, &mut dag, &[query], resolve_revset_options) { 64 | Ok(commit_sets) => commit_sets[0].clone(), 65 | Err(err) => { 66 | err.describe(effects)?; 67 | return Ok(Err(ExitCode(1))); 68 | } 69 | }; 70 | 71 | if show_branches { 72 | let commit_oids = { 73 | let (effects, _progress) = effects.start_operation(OperationType::SortCommits); 74 | let _effects = effects; 75 | 76 | let commit_set = commit_set.intersection(&dag.branch_commits); 77 | dag.sort(&commit_set)? 78 | }; 79 | let ref_names = commit_oids 80 | .into_iter() 81 | .flat_map( 82 | |oid| match references_snapshot.branch_oid_to_names.get(&oid) { 83 | Some(branch_names) => branch_names.iter().sorted().collect_vec(), 84 | None => Vec::new(), 85 | }, 86 | ) 87 | .collect_vec(); 88 | for ref_name in ref_names { 89 | let ref_name = CategorizedReferenceName::new(ref_name); 90 | writeln!(effects.get_output_stream(), "{}", ref_name.render_suffix())?; 91 | } 92 | } else { 93 | let commit_oids = { 94 | let (effects, _progress) = effects.start_operation(OperationType::SortCommits); 95 | let _effects = effects; 96 | dag.sort(&commit_set)? 97 | }; 98 | for commit_oid in commit_oids { 99 | if raw { 100 | writeln!(effects.get_output_stream(), "{commit_oid}")?; 101 | } else { 102 | let commit = repo.find_commit_or_fail(commit_oid)?; 103 | writeln!( 104 | effects.get_output_stream(), 105 | "{}", 106 | effects 107 | .get_glyphs() 108 | .render(commit.friendly_describe(effects.get_glyphs())?)?, 109 | )?; 110 | } 111 | } 112 | } 113 | 114 | Ok(Ok(())) 115 | } 116 | -------------------------------------------------------------------------------- /scm-bisect/src/testing.rs: -------------------------------------------------------------------------------- 1 | //! Testing utilities. 2 | 3 | use std::collections::{HashMap, HashSet}; 4 | use std::convert::Infallible; 5 | 6 | use itertools::Itertools; 7 | use proptest::prelude::Strategy as ProptestStrategy; 8 | use proptest::prelude::*; 9 | 10 | use crate::basic::{BasicSourceControlGraph, BasicStrategyKind}; 11 | 12 | /// Graph that represents a "stick" of nodes, represented as increasing 13 | /// integers. The node `n` is the immediate parent of `n + 1`. 14 | #[derive(Clone, Debug)] 15 | pub struct UsizeGraph { 16 | /// The maximum node value for this graph. Valid nodes are in `0..max` (a 17 | /// half-open [`std::ops::Range`]). 18 | pub max: usize, 19 | } 20 | 21 | impl BasicSourceControlGraph for UsizeGraph { 22 | type Node = usize; 23 | type Error = Infallible; 24 | 25 | fn ancestors(&self, node: Self::Node) -> Result, Infallible> { 26 | assert!(node < self.max); 27 | Ok((0..=node).collect()) 28 | } 29 | 30 | fn descendants(&self, node: Self::Node) -> Result, Infallible> { 31 | assert!(node < self.max); 32 | Ok((node..self.max).collect()) 33 | } 34 | } 35 | 36 | /// Directed acyclic graph with nodes `char` and edges `char -> char`. 37 | #[derive(Clone, Debug)] 38 | pub struct TestGraph { 39 | /// Mapping from parent to children. 40 | pub nodes: HashMap>, 41 | } 42 | 43 | impl BasicSourceControlGraph for TestGraph { 44 | type Node = char; 45 | type Error = Infallible; 46 | 47 | fn ancestors(&self, node: Self::Node) -> Result, Infallible> { 48 | let mut result = HashSet::new(); 49 | result.insert(node); 50 | let parents: HashSet = self 51 | .nodes 52 | .iter() 53 | .filter_map(|(k, v)| if v.contains(&node) { Some(*k) } else { None }) 54 | .collect(); 55 | result.extend(self.ancestors_all(parents)?); 56 | Ok(result) 57 | } 58 | 59 | fn descendants(&self, node: Self::Node) -> Result, Infallible> { 60 | let mut result = HashSet::new(); 61 | result.insert(node); 62 | let children: HashSet = self.nodes[&node].clone(); 63 | result.extend(self.descendants_all(children)?); 64 | Ok(result) 65 | } 66 | } 67 | 68 | /// Select an arbitrary [`BasicStrategyKind`]. 69 | pub fn arb_strategy() -> impl ProptestStrategy { 70 | prop_oneof![ 71 | Just(BasicStrategyKind::Linear), 72 | Just(BasicStrategyKind::LinearReverse), 73 | Just(BasicStrategyKind::Binary), 74 | ] 75 | } 76 | 77 | /// Create an arbitrary [`TestGraph`] and an arbitrary set of failing nodes. 78 | pub fn arb_test_graph_and_nodes() -> impl ProptestStrategy)> { 79 | let nodes = prop::collection::hash_set( 80 | prop::sample::select(vec!['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h']), 81 | 1..=8, 82 | ); 83 | nodes 84 | .prop_flat_map(|nodes| { 85 | let num_nodes = nodes.len(); 86 | let nodes_kv = nodes 87 | .iter() 88 | .copied() 89 | .map(|node| (node, HashSet::new())) 90 | .collect(); 91 | let graph = TestGraph { nodes: nodes_kv }; 92 | let lineages = prop::collection::vec( 93 | prop::sample::subsequence(nodes.into_iter().collect_vec(), 0..num_nodes), 94 | 0..num_nodes, 95 | ); 96 | (Just(graph), lineages) 97 | }) 98 | .prop_map(|(mut graph, lineages)| { 99 | for lineage in lineages { 100 | for (parent, child) in lineage.into_iter().tuple_windows() { 101 | graph.nodes.get_mut(&parent).unwrap().insert(child); 102 | } 103 | } 104 | graph 105 | }) 106 | .prop_flat_map(|graph| { 107 | let nodes = graph.nodes.keys().copied().collect::>(); 108 | let num_nodes = nodes.len(); 109 | let failure_nodes = prop::sample::subsequence(nodes, 0..num_nodes); 110 | (Just(graph), failure_nodes) 111 | }) 112 | } 113 | -------------------------------------------------------------------------------- /scm-bisect/examples/guessing_game.rs: -------------------------------------------------------------------------------- 1 | use std::cmp::Ordering; 2 | use std::convert::Infallible; 3 | use std::io; 4 | use std::ops::RangeInclusive; 5 | 6 | use indexmap::IndexMap; 7 | use scm_bisect::search; 8 | 9 | type Node = isize; 10 | 11 | #[derive(Debug)] 12 | struct Graph; 13 | 14 | impl search::Graph for Graph { 15 | type Node = Node; 16 | 17 | type Error = Infallible; 18 | 19 | fn is_ancestor( 20 | &self, 21 | ancestor: Self::Node, 22 | descendant: Self::Node, 23 | ) -> Result { 24 | // Note that a node is always considered an ancestor of itself. 25 | Ok(ancestor <= descendant) 26 | } 27 | } 28 | 29 | #[derive(Debug)] 30 | struct Strategy { 31 | range: RangeInclusive, 32 | } 33 | 34 | impl search::Strategy for Strategy { 35 | type Error = Infallible; 36 | 37 | fn midpoint( 38 | &self, 39 | _graph: &Graph, 40 | bounds: &search::Bounds, 41 | _statuses: &IndexMap, 42 | ) -> Result, Self::Error> { 43 | let search::Bounds { 44 | success: success_bounds, 45 | failure: failure_bounds, 46 | } = bounds; 47 | let lower_bound = success_bounds 48 | .iter() 49 | .max() 50 | .copied() 51 | .unwrap_or_else(|| self.range.start() - 1); 52 | let upper_bound = failure_bounds 53 | .iter() 54 | .min() 55 | .copied() 56 | .unwrap_or_else(|| self.range.end() + 1); 57 | let midpoint = if lower_bound < upper_bound - 1 { 58 | (lower_bound + upper_bound) / 2 59 | } else { 60 | return Ok(None); 61 | }; 62 | assert!(self.range.contains(&midpoint)); 63 | Ok(Some(midpoint)) 64 | } 65 | } 66 | 67 | fn play(mut read_input: impl FnMut(isize) -> Result) -> Result, E> { 68 | let search_range = 0..=100; 69 | let mut search = search::Search::new(Graph, search_range.clone()); 70 | let strategy = Strategy { 71 | range: search_range, 72 | }; 73 | 74 | let result = loop { 75 | let guess = { 76 | let mut guess = search.search(&strategy).unwrap(); 77 | match guess.next_to_search.next() { 78 | Some(guess) => guess.unwrap(), 79 | None => { 80 | break None; 81 | } 82 | } 83 | }; 84 | let input = read_input(guess)?; 85 | match input { 86 | Ordering::Less => search.notify(guess, search::Status::Failure).unwrap(), 87 | Ordering::Greater => search.notify(guess, search::Status::Success).unwrap(), 88 | Ordering::Equal => { 89 | break Some(guess); 90 | } 91 | } 92 | }; 93 | Ok(result) 94 | } 95 | 96 | fn main() -> io::Result<()> { 97 | println!("Think of a number between 0 and 100."); 98 | let result = play(|guess| -> io::Result<_> { 99 | println!("Is your number {guess}? [<=>]"); 100 | let result = loop { 101 | let mut input = String::new(); 102 | std::io::stdin().read_line(&mut input)?; 103 | match input.trim() { 104 | "<" => break Ordering::Less, 105 | "=" => break Ordering::Equal, 106 | ">" => break Ordering::Greater, 107 | _ => println!("Please enter '<', '=', or '>'."), 108 | } 109 | }; 110 | Ok(result) 111 | })?; 112 | match result { 113 | Some(result) => println!("I win! Your number was: {result}"), 114 | None => println!("I give up!"), 115 | } 116 | Ok(()) 117 | } 118 | 119 | #[cfg(test)] 120 | mod tests { 121 | use super::*; 122 | 123 | proptest::proptest! { 124 | #[test] 125 | fn test_no_crashes_on_valid_input(inputs: Vec) { 126 | struct Exit; 127 | let mut iter = inputs.into_iter(); 128 | let _: Result, Exit> = play(move |_| iter.next().ok_or(Exit)); 129 | } 130 | 131 | #[test] 132 | fn test_finds_number(input in 0..=100_isize) { 133 | let result = play(|guess| -> Result { Ok(input.cmp(&guess)) }); 134 | assert_eq!(result, Ok(Some(input))); 135 | } 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /git-branchless-revset/src/grammar.lalrpop: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | use super::ast::Expr; 3 | 4 | grammar; 5 | 6 | // Below implements a hierarchy of operator precedences. The lower-numbered 7 | // `Expr`s bind less tightly than the higher-numbered `Expr`s. 8 | 9 | pub Expr: Expr<'input> = { 10 | "|" => Expr::FunctionCall(Cow::Borrowed("union"), vec![lhs, rhs]), 11 | "+" => Expr::FunctionCall(Cow::Borrowed("union"), vec![lhs, rhs]), 12 | "or" => Expr::FunctionCall(Cow::Borrowed("union"), vec![lhs, rhs]), 13 | , 14 | } 15 | 16 | Expr2: Expr<'input> = { 17 | "&" => Expr::FunctionCall(Cow::Borrowed("intersection"), vec![lhs, rhs]), 18 | "and" => Expr::FunctionCall(Cow::Borrowed("intersection"), vec![lhs, rhs]), 19 | "-" => Expr::FunctionCall(Cow::Borrowed("difference"), vec![lhs, rhs]), 20 | "%" => Expr::FunctionCall(Cow::Borrowed("only"), vec![lhs, rhs]), 21 | 22 | } 23 | 24 | Expr3: Expr<'input> = { 25 | ":" => Expr::FunctionCall(Cow::Borrowed("range"), vec![lhs, rhs]), 26 | ":" => Expr::FunctionCall(Cow::Borrowed("descendants"), vec![lhs, ]), 27 | ":" => Expr::FunctionCall(Cow::Borrowed("ancestors"), vec![ rhs]), 28 | 29 | // For Mercurial users' familiarity. 30 | "::" => Expr::FunctionCall(Cow::Borrowed("range"), vec![lhs, rhs]), 31 | "::" => Expr::FunctionCall(Cow::Borrowed("descendants"), vec![lhs, ]), 32 | "::" => Expr::FunctionCall(Cow::Borrowed("ancestors"), vec![ rhs]), 33 | 34 | // Note that the LHS and RHS are passed in opposite order to `only`. 35 | ".." => Expr::FunctionCall(Cow::Borrowed("only"), vec![rhs, lhs]), 36 | ".." => Expr::FunctionCall(Cow::Borrowed("only"), vec![Expr::Name(Cow::Borrowed(".")), lhs]), 37 | ".." => Expr::FunctionCall(Cow::Borrowed("only"), vec![rhs, Expr::Name(Cow::Borrowed("."))]), 38 | 39 | 40 | } 41 | 42 | Expr4: Expr<'input> = { 43 | "^" => Expr::FunctionCall(Cow::Borrowed("parents.nth"), vec![lhs, Expr::Name(Cow::Borrowed("1"))]), 44 | "^" => Expr::FunctionCall(Cow::Borrowed("parents.nth"), vec![lhs, Expr::Name(rhs)]), 45 | 46 | "~" => Expr::FunctionCall(Cow::Borrowed("ancestors.nth"), vec![lhs, Expr::Name(Cow::Borrowed("1"))]), 47 | "~" => Expr::FunctionCall(Cow::Borrowed("ancestors.nth"), vec![lhs, Expr::Name(rhs)]), 48 | 49 | 50 | } 51 | 52 | Expr5: Expr<'input> = { 53 | "(" ")", 54 | "(" ")" => Expr::FunctionCall(name, args), 55 | => Expr::Name(name), 56 | } 57 | 58 | Name: Cow<'input, str> = { 59 | // NOTE: `.` should be allowed in names, but not `..` (since that is an operator). 60 | => Cow::Borrowed(&s), 61 | 62 | // Escaping is not supported in LALROP regex literals. 63 | // \x22 = double-quote. 64 | // \x27 = single-quote. 65 | // \x5c = backslash 66 | => { 67 | assert!(s.len() >= 2, "There should be at least 2 quote characters in the string literal, got: {}", s); 68 | 69 | let mut result = String::new(); 70 | let mut last_char_was_backslash = false; 71 | for c in s.chars().skip(1) { 72 | if last_char_was_backslash { 73 | result.push(match c { 74 | 'n' => '\n', 75 | 'r' => '\r', 76 | 't' => '\t', 77 | c => c, 78 | }); 79 | } else if c == '\\' { 80 | last_char_was_backslash = true; 81 | } else { 82 | result.push(c); 83 | } 84 | } 85 | result.pop(); 86 | Cow::Owned(result) 87 | }, 88 | } 89 | 90 | FunctionArgs: Vec> = { 91 | ",")*> => { 92 | let mut exprs = exprs; 93 | if let Some(last_expr) = last_expr { 94 | exprs.push(last_expr); 95 | } 96 | exprs 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /git-branchless-undo/src/tui/testing.rs: -------------------------------------------------------------------------------- 1 | //! Testing helpers for interactive interfaces. 2 | 3 | use std::borrow::Borrow; 4 | use std::cell::RefCell; 5 | use std::rc::Rc; 6 | 7 | use cursive::backend::Backend; 8 | use cursive::theme::Color; 9 | 10 | /// Represents a "screenshot" of the terminal taken at a point in time. 11 | pub type Screen = Vec>; 12 | 13 | /// The kind of events that can be 14 | #[derive(Clone, Debug)] 15 | pub enum CursiveTestingEvent { 16 | /// A regular Cursive event. 17 | Event(cursive::event::Event), 18 | 19 | /// Take a screenshot at the current point in time and store it in the 20 | /// provided screenshot cell. 21 | TakeScreenshot(Rc>), 22 | } 23 | 24 | /// The testing backend. It feeds a predetermined list of events to the 25 | /// Cursive event loop and stores a virtual terminal for Cursive to draw on. 26 | #[derive(Debug)] 27 | pub struct CursiveTestingBackend { 28 | events: Vec, 29 | event_index: usize, 30 | just_emitted_event: bool, 31 | screen: RefCell, 32 | } 33 | 34 | impl CursiveTestingBackend { 35 | /// Construct the testing backend with the provided set of events. 36 | pub fn init(events: Vec) -> Box { 37 | Box::new(CursiveTestingBackend { 38 | events, 39 | event_index: 0, 40 | just_emitted_event: false, 41 | screen: RefCell::new(vec![vec![' '; 120]; 24]), 42 | }) 43 | } 44 | } 45 | 46 | impl Backend for CursiveTestingBackend { 47 | fn poll_event(&mut self) -> Option { 48 | // Cursive will poll all available events. We only want it to 49 | // process events one at a time, so return `None` after each event. 50 | if self.just_emitted_event { 51 | self.just_emitted_event = false; 52 | return None; 53 | } 54 | 55 | let event_index = self.event_index; 56 | self.event_index += 1; 57 | match self.events.get(event_index)?.to_owned() { 58 | CursiveTestingEvent::TakeScreenshot(screen_target) => { 59 | let mut screen_target = (*screen_target).borrow_mut(); 60 | screen_target.clone_from(&self.screen.borrow()); 61 | self.poll_event() 62 | } 63 | CursiveTestingEvent::Event(event) => { 64 | self.just_emitted_event = true; 65 | Some(event) 66 | } 67 | } 68 | } 69 | 70 | fn refresh(&mut self) {} 71 | 72 | fn has_colors(&self) -> bool { 73 | false 74 | } 75 | 76 | fn screen_size(&self) -> cursive::Vec2 { 77 | let screen = self.screen.borrow(); 78 | (screen[0].len(), screen.len()).into() 79 | } 80 | 81 | fn print_at(&self, pos: cursive::Vec2, text: &str) { 82 | for (i, c) in text.chars().enumerate() { 83 | let mut screen = self.screen.borrow_mut(); 84 | let screen_width = screen[0].len(); 85 | if pos.x + i < screen_width { 86 | screen[pos.y][pos.x + i] = c; 87 | } else { 88 | // Indicate that the screen was overfull. 89 | screen[pos.y][screen_width - 1] = '$'; 90 | } 91 | } 92 | } 93 | 94 | fn clear(&self, _color: Color) { 95 | let mut screen = self.screen.borrow_mut(); 96 | for i in 0..screen.len() { 97 | for j in 0..screen[i].len() { 98 | screen[i][j] = ' '; 99 | } 100 | } 101 | } 102 | 103 | fn set_color(&self, colors: cursive::theme::ColorPair) -> cursive::theme::ColorPair { 104 | colors 105 | } 106 | 107 | fn set_effect(&self, _effect: cursive::theme::Effect) {} 108 | 109 | fn unset_effect(&self, _effect: cursive::theme::Effect) {} 110 | 111 | fn set_title(&mut self, _title: String) {} 112 | } 113 | 114 | /// Convert the screenshot into a string for assertions, such as for use 115 | /// with `insta::assert_snapshot!`. 116 | pub fn screen_to_string(screen: &Rc>) -> String { 117 | let screen = Rc::borrow(screen); 118 | let screen = RefCell::borrow(screen); 119 | screen 120 | .iter() 121 | .map(|row| { 122 | let line: String = row.iter().collect(); 123 | line.trim_end().to_owned() + "\n" 124 | }) 125 | .collect::() 126 | .trim() 127 | .to_owned() 128 | } 129 | -------------------------------------------------------------------------------- /git-branchless-submit/examples/dump_phabricator_dependencies.rs: -------------------------------------------------------------------------------- 1 | use std::{collections::HashSet, path::PathBuf}; 2 | 3 | use clap::Parser; 4 | use git_branchless_opts::{ResolveRevsetOptions, Revset}; 5 | use git_branchless_revset::resolve_commits; 6 | use git_branchless_submit::phabricator; 7 | use lib::core::dag::{sorted_commit_set, Dag}; 8 | use lib::core::effects::Effects; 9 | use lib::core::eventlog::{EventLogDb, EventReplayer}; 10 | use lib::core::formatting::Glyphs; 11 | use lib::core::repo_ext::RepoExt; 12 | use lib::git::{GitRunInfo, NonZeroOid, Repo}; 13 | 14 | #[derive(Debug, Parser)] 15 | struct Opts { 16 | #[clap(short = 'C', long = "working-directory")] 17 | working_directory: Option, 18 | 19 | #[clap(default_value = ".")] 20 | revset: Revset, 21 | } 22 | 23 | fn main() -> eyre::Result<()> { 24 | let Opts { 25 | working_directory, 26 | revset, 27 | } = Opts::try_parse()?; 28 | match working_directory { 29 | Some(working_directory) => { 30 | std::env::set_current_dir(working_directory)?; 31 | } 32 | None => { 33 | eprintln!("Warning: --working-directory was not passed, so running in current directory. But git-branchless is not hosted on Phabricator, so this seems unlikely to be useful, since there will be no assocated Phabricator information. Try running in your repository."); 34 | } 35 | } 36 | 37 | let effects = Effects::new(Glyphs::detect()); 38 | let git_run_info = GitRunInfo { 39 | path_to_git: "git".into(), 40 | working_directory: std::env::current_dir()?, 41 | env: Default::default(), 42 | }; 43 | let repo = Repo::from_current_dir()?; 44 | let conn = repo.get_db_conn()?; 45 | let event_log_db = EventLogDb::new(&conn)?; 46 | let event_replayer = EventReplayer::from_event_log_db(&effects, &repo, &event_log_db)?; 47 | let event_cursor = event_replayer.make_default_cursor(); 48 | let references_snapshot = repo.get_references_snapshot()?; 49 | let mut dag = Dag::open_and_sync( 50 | &effects, 51 | &repo, 52 | &event_replayer, 53 | event_cursor, 54 | &references_snapshot, 55 | )?; 56 | 57 | let resolved_commits = resolve_commits( 58 | &effects, 59 | &repo, 60 | &mut dag, 61 | &[revset.clone()], 62 | &ResolveRevsetOptions { 63 | show_hidden_commits: false, 64 | }, 65 | )?; 66 | let commits = match resolved_commits.as_slice() { 67 | [commits] => commits, 68 | other => eyre::bail!("Unexpected number of returned commit sets for {revset}: {other:?}"), 69 | }; 70 | let commit_oids: HashSet = dag.commit_set_to_vec(commits)?.into_iter().collect(); 71 | 72 | let commits = sorted_commit_set(&repo, &dag, commits)?; 73 | let phabricator = phabricator::PhabricatorForge { 74 | effects: &effects, 75 | git_run_info: &git_run_info, 76 | repo: &repo, 77 | dag: &mut dag, 78 | event_log_db: &event_log_db, 79 | revset: &revset, 80 | }; 81 | let dependency_oids = phabricator.query_remote_dependencies(commit_oids)?; 82 | for commit in commits { 83 | println!( 84 | "Dependencies for {} {}:", 85 | revision_id_str(&phabricator, commit.get_oid())?, 86 | effects 87 | .get_glyphs() 88 | .render(commit.friendly_describe(effects.get_glyphs())?)? 89 | ); 90 | 91 | let dependency_oids = &dependency_oids[&commit.get_oid()]; 92 | if dependency_oids.is_empty() { 93 | println!("- "); 94 | } else { 95 | for dependency_oid in dependency_oids { 96 | let dependency_commit = repo.find_commit_or_fail(*dependency_oid)?; 97 | println!( 98 | "- {} {}", 99 | revision_id_str(&phabricator, *dependency_oid)?, 100 | effects 101 | .get_glyphs() 102 | .render(dependency_commit.friendly_describe(effects.get_glyphs())?)? 103 | ); 104 | } 105 | } 106 | } 107 | Ok(()) 108 | } 109 | 110 | fn revision_id_str( 111 | phabricator: &phabricator::PhabricatorForge, 112 | commit_oid: NonZeroOid, 113 | ) -> eyre::Result { 114 | let id = phabricator.get_revision_id(commit_oid)?; 115 | let revision_id = match id.as_ref() { 116 | Some(phabricator::Id(id)) => id, 117 | None => "???", 118 | }; 119 | Ok(format!("D{revision_id}")) 120 | } 121 | -------------------------------------------------------------------------------- /git-branchless-revset/src/resolve.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Write; 2 | 3 | use eyre::WrapErr; 4 | use git_branchless_opts::{ResolveRevsetOptions, Revset}; 5 | use lib::core::config::get_smartlog_default_revset; 6 | use lib::core::dag::{CommitSet, Dag}; 7 | use lib::core::effects::Effects; 8 | use lib::git::Repo; 9 | use thiserror::Error; 10 | use tracing::instrument; 11 | 12 | use crate::eval::EvalError; 13 | use crate::parser::ParseError; 14 | use crate::Expr; 15 | use crate::{eval, parse}; 16 | 17 | /// The result of attempting to resolve commits. 18 | #[allow(clippy::enum_variant_names)] 19 | #[derive(Debug, Error)] 20 | pub enum ResolveError { 21 | #[error("parse error in {expr:?}: {source}")] 22 | ParseError { expr: String, source: ParseError }, 23 | 24 | #[error("evaluation error in {expr:?}: {source}")] 25 | EvalError { expr: String, source: EvalError }, 26 | 27 | #[error("DAG query error: {source}")] 28 | DagError { source: eden_dag::Error }, 29 | 30 | #[error(transparent)] 31 | OtherError { source: eyre::Error }, 32 | } 33 | 34 | impl ResolveError { 35 | pub fn describe(self, effects: &Effects) -> eyre::Result<()> { 36 | match self { 37 | ResolveError::ParseError { expr, source } => { 38 | writeln!( 39 | effects.get_error_stream(), 40 | "Parse error for expression '{expr}': {source}" 41 | )?; 42 | Ok(()) 43 | } 44 | ResolveError::EvalError { expr, source } => { 45 | writeln!( 46 | effects.get_error_stream(), 47 | "Evaluation error for expression '{expr}': {source}" 48 | )?; 49 | Ok(()) 50 | } 51 | ResolveError::DagError { source } => Err(source.into()), 52 | ResolveError::OtherError { source } => Err(source), 53 | } 54 | } 55 | } 56 | 57 | /// Check for syntax errors in the provided revsets without actually evaluating them. 58 | pub fn check_revset_syntax(repo: &Repo, revsets: &[Revset]) -> Result<(), ParseError> { 59 | for Revset(revset) in revsets { 60 | if let Ok(Some(_)) = repo.revparse_single_commit(revset) { 61 | continue; 62 | } 63 | let _expr: Expr = parse(revset)?; 64 | } 65 | Ok(()) 66 | } 67 | 68 | /// Parse strings which refer to commits, such as: 69 | /// 70 | /// - Full OIDs. 71 | /// - Short OIDs. 72 | /// - Reference names. 73 | #[instrument] 74 | pub fn resolve_commits( 75 | effects: &Effects, 76 | repo: &Repo, 77 | dag: &mut Dag, 78 | revsets: &[Revset], 79 | options: &ResolveRevsetOptions, 80 | ) -> Result, ResolveError> { 81 | let mut dag_with_obsolete = if options.show_hidden_commits { 82 | Some( 83 | dag.clear_obsolete_commits(repo) 84 | .map_err(|err| ResolveError::OtherError { source: err })?, 85 | ) 86 | } else { 87 | None 88 | }; 89 | let dag = dag_with_obsolete.as_mut().unwrap_or(dag); 90 | 91 | let mut commit_sets = Vec::new(); 92 | for Revset(revset) in revsets { 93 | // NB: also update `check_parse_revsets` 94 | 95 | // Handle syntax that's supported by Git, but which we haven't 96 | // implemented in the revset language. 97 | if let Ok(Some(commit)) = repo.revparse_single_commit(revset) { 98 | let commit_set = CommitSet::from(commit.get_oid()); 99 | dag.sync_from_oids(effects, repo, CommitSet::empty(), commit_set.clone()) 100 | .map_err(|err| ResolveError::OtherError { source: err })?; 101 | commit_sets.push(commit_set); 102 | continue; 103 | } 104 | 105 | let expr = parse(revset).map_err(|err| ResolveError::ParseError { 106 | expr: revset.clone(), 107 | source: err, 108 | })?; 109 | let commits = eval(effects, repo, dag, &expr).map_err(|err| ResolveError::EvalError { 110 | expr: revset.clone(), 111 | source: err, 112 | })?; 113 | 114 | commit_sets.push(commits); 115 | } 116 | Ok(commit_sets) 117 | } 118 | 119 | /// Resolve the set of commits that would appear in the smartlog by default (if 120 | /// the user doesn't specify a revset). 121 | pub fn resolve_default_smartlog_commits( 122 | effects: &Effects, 123 | repo: &Repo, 124 | dag: &mut Dag, 125 | ) -> eyre::Result { 126 | let revset = Revset(get_smartlog_default_revset(repo)?); 127 | let results = resolve_commits( 128 | effects, 129 | repo, 130 | dag, 131 | &[revset], 132 | &ResolveRevsetOptions::default(), 133 | ) 134 | .wrap_err("Resolving default smartlog commits")?; 135 | let commits = results.first().unwrap(); 136 | Ok(commits.clone()) 137 | } 138 | -------------------------------------------------------------------------------- /git-branchless/src/commands/repair.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Write; 2 | use std::{collections::HashSet, time::SystemTime}; 3 | 4 | use itertools::Itertools; 5 | use lib::core::effects::WithProgress; 6 | use lib::git::{CategorizedReferenceName, MaybeZeroOid}; 7 | use lib::util::EyreExitOr; 8 | use lib::{ 9 | core::{ 10 | effects::{Effects, OperationType}, 11 | eventlog::{Event, EventLogDb, EventReplayer}, 12 | formatting::Pluralize, 13 | }, 14 | git::Repo, 15 | }; 16 | 17 | pub fn repair(effects: &Effects, dry_run: bool) -> EyreExitOr<()> { 18 | let repo = Repo::from_current_dir()?; 19 | let conn = repo.get_db_conn()?; 20 | let event_log_db = EventLogDb::new(&conn)?; 21 | let event_replayer = EventReplayer::from_event_log_db(effects, &repo, &event_log_db)?; 22 | let event_cursor = event_replayer.make_default_cursor(); 23 | 24 | let broken_commits = { 25 | let (effects, progress) = effects.start_operation(OperationType::RepairCommits); 26 | let _effects = effects; 27 | let cursor_oids = event_replayer.get_cursor_oids(event_cursor); 28 | let mut result = HashSet::new(); 29 | for oid in cursor_oids.into_iter().with_progress(progress) { 30 | if repo.find_commit(oid)?.is_none() { 31 | result.insert(oid); 32 | } 33 | } 34 | result 35 | }; 36 | 37 | let broken_branches = { 38 | let (effects, progress) = effects.start_operation(OperationType::RepairBranches); 39 | let _effects = effects; 40 | let references_snapshot = event_replayer.get_references_snapshot(&repo, event_cursor)?; 41 | let branch_names = references_snapshot 42 | .branch_oid_to_names 43 | .into_iter() 44 | .flat_map(|(oid, reference_names)| { 45 | reference_names 46 | .into_iter() 47 | .map(move |reference_name| (oid, reference_name)) 48 | }) 49 | .collect_vec(); 50 | let mut result = HashSet::new(); 51 | for (oid, reference_name) in branch_names.into_iter().with_progress(progress) { 52 | if repo.find_reference(&reference_name)?.is_none() { 53 | result.insert((oid, reference_name)); 54 | } 55 | } 56 | result 57 | }; 58 | 59 | let now = SystemTime::now(); 60 | let timestamp = now.duration_since(SystemTime::UNIX_EPOCH)?.as_secs_f64(); 61 | let event_tx_id = event_log_db.make_transaction_id(now, "repair")?; 62 | 63 | let num_broken_commits = broken_commits.len(); 64 | let commit_events = broken_commits 65 | .iter() 66 | .map(|commit_oid| Event::ObsoleteEvent { 67 | timestamp, 68 | event_tx_id, 69 | commit_oid: *commit_oid, 70 | }) 71 | .collect_vec(); 72 | let num_broken_branches = broken_branches.len(); 73 | let branch_events = 74 | broken_branches 75 | .iter() 76 | .map(|(old_oid, reference_name)| Event::RefUpdateEvent { 77 | timestamp, 78 | event_tx_id, 79 | ref_name: reference_name.to_owned(), 80 | old_oid: MaybeZeroOid::NonZero(*old_oid), 81 | new_oid: MaybeZeroOid::Zero, 82 | message: None, 83 | }); 84 | 85 | if !dry_run { 86 | let events = commit_events.into_iter().chain(branch_events).collect_vec(); 87 | event_log_db.add_events(events)?; 88 | } 89 | 90 | if num_broken_commits > 0 { 91 | writeln!( 92 | effects.get_output_stream(), 93 | "Found and repaired {}: {}", 94 | Pluralize { 95 | determiner: None, 96 | amount: num_broken_commits, 97 | unit: ("broken commit", "broken commits") 98 | }, 99 | broken_commits.into_iter().sorted().join(", "), 100 | )?; 101 | } 102 | if num_broken_branches > 0 { 103 | writeln!( 104 | effects.get_output_stream(), 105 | "Found and repaired {}: {}", 106 | Pluralize { 107 | determiner: None, 108 | amount: num_broken_branches, 109 | unit: ("broken branch", "broken branches") 110 | }, 111 | broken_branches 112 | .into_iter() 113 | .map( 114 | |(_oid, reference_name)| CategorizedReferenceName::new(&reference_name) 115 | .render_suffix() 116 | ) 117 | .sorted() 118 | .join(", "), 119 | )?; 120 | } 121 | 122 | if dry_run { 123 | writeln!( 124 | effects.get_output_stream(), 125 | "(This was a dry-run; run with --no-dry-run to apply changes.)" 126 | )?; 127 | } 128 | 129 | Ok(Ok(())) 130 | } 131 | -------------------------------------------------------------------------------- /git-branchless-lib/src/core/task.rs: -------------------------------------------------------------------------------- 1 | //! Utilities for compute-heavy tasks which need to be run in parallel. 2 | 3 | use std::ops::Deref; 4 | use std::sync::Mutex; 5 | 6 | /// A factory which produces a resource for use with [`ResourcePool`]. 7 | pub trait Resource { 8 | /// The type of the resource to be produced. 9 | type Output; 10 | 11 | /// An error type. 12 | type Error; 13 | 14 | /// Constructor for the resource. 15 | fn try_create(&self) -> Result; 16 | } 17 | 18 | /// An unbounded pool of on-demand generated resources. This is useful when 19 | /// distributing work across threads which needs access to an expensive-to-build 20 | /// context. 21 | /// 22 | /// A new resource is created when there isn't one available in the pool. When 23 | /// it's dropped, it's returned to the pool. Old resources remain in the pool 24 | /// until the pool itself is dropped. 25 | /// 26 | /// ``` 27 | /// # use std::cell::RefCell; 28 | /// # use branchless::core::task::{Resource, ResourceHandle, ResourcePool}; 29 | /// struct MyResource { 30 | /// num_instantiations: RefCell, 31 | /// } 32 | /// 33 | /// impl Resource for MyResource { 34 | /// type Output = String; 35 | /// type Error = std::convert::Infallible; 36 | /// fn try_create(&self) -> Result { 37 | /// let mut r = self.num_instantiations.borrow_mut(); 38 | /// *r += 1; 39 | /// Ok(format!("This is resource #{}", *r)) 40 | /// } 41 | /// } 42 | /// 43 | /// # fn main() { 44 | /// let resource = MyResource { num_instantiations: Default::default() }; 45 | /// let pool = ResourcePool::new(resource); 46 | /// 47 | /// // Any number of the resource can be created. 48 | /// let r1: ResourceHandle<'_, MyResource> = pool.try_create().unwrap(); 49 | /// assert_eq!(&*r1, "This is resource #1"); 50 | /// let r2 = pool.try_create().unwrap(); 51 | /// assert_eq!(&*r2, "This is resource #2"); 52 | /// drop(r2); 53 | /// drop(r1); 54 | /// 55 | /// // After releasing a resource, an attempt to get a resource returns an 56 | /// // existing one from the pool. 57 | /// let r1_again = pool.try_create().unwrap(); 58 | /// assert_eq!(&*r1_again, "This is resource #1"); 59 | /// # } 60 | /// ``` 61 | pub struct ResourcePool { 62 | factory: R, 63 | resources: Mutex>, 64 | } 65 | 66 | impl std::fmt::Debug for ResourcePool { 67 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 68 | f.debug_struct("ResourcePool") 69 | .field("factory", &"") 70 | .field( 71 | "resources.len()", 72 | &match self.resources.try_lock() { 73 | Ok(resources) => resources.len().to_string(), 74 | Err(_) => "".to_string(), 75 | }, 76 | ) 77 | .finish() 78 | } 79 | } 80 | 81 | /// A handle to an instance of a resource created by [`Resource::try_create`]. 82 | /// When this value is dropped, the underlying resource returns to the owning 83 | /// [`ResourcePool`]. 84 | pub struct ResourceHandle<'pool, R: Resource> { 85 | parent: &'pool ResourcePool, 86 | inner: Option, 87 | } 88 | 89 | impl Drop for ResourceHandle<'_, R> { 90 | fn drop(&mut self) { 91 | let mut resources = self 92 | .parent 93 | .resources 94 | .lock() 95 | .expect("Poisoned mutex for ResourceHandle"); 96 | resources.push(self.inner.take().unwrap()); 97 | } 98 | } 99 | 100 | impl Deref for ResourceHandle<'_, R> { 101 | type Target = R::Output; 102 | 103 | fn deref(&self) -> &Self::Target { 104 | self.inner.as_ref().unwrap() 105 | } 106 | } 107 | 108 | impl ResourcePool { 109 | /// Constructor. 110 | pub fn new(factory: R) -> Self { 111 | ResourcePool { 112 | factory, 113 | resources: Default::default(), 114 | } 115 | } 116 | 117 | /// If there are any resources available in the pool, return an arbitrary 118 | /// one. Otherwise, invoke the constructor function of the associated 119 | /// [`Resource`] and return a [`ResourceHandle`] for it. 120 | pub fn try_create(&self) -> Result, R::Error> { 121 | let resource = { 122 | let mut resources = self 123 | .resources 124 | .lock() 125 | .expect("Poisoned mutex for ResourcePool"); 126 | let resource = resources.pop(); 127 | match resource { 128 | Some(resource) => resource, 129 | None => self.factory.try_create()?, 130 | } 131 | }; 132 | Ok(ResourceHandle { 133 | parent: self, 134 | inner: Some(resource), 135 | }) 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /git-branchless-lib/src/core/gc.rs: -------------------------------------------------------------------------------- 1 | //! Deal with Git's garbage collection mechanism. 2 | //! 3 | //! Git treats a commit as unreachable if there are no references that point to 4 | //! it or one of its descendants. However, the branchless workflow requires 5 | //! keeping such commits reachable until the user has obsoleted them. 6 | //! 7 | //! This module is responsible for adding extra references to Git, so that Git's 8 | //! garbage collection doesn't collect commits which branchless thinks are still 9 | //! active. 10 | 11 | use std::fmt::Write; 12 | 13 | use eyre::Context; 14 | use tracing::instrument; 15 | 16 | use crate::core::effects::Effects; 17 | use crate::core::eventlog::{ 18 | is_gc_ref, CommitActivityStatus, EventCursor, EventLogDb, EventReplayer, 19 | }; 20 | use crate::core::formatting::Pluralize; 21 | use crate::git::{NonZeroOid, Reference, Repo}; 22 | 23 | /// Find references under `refs/branchless/` which point to commits which are no 24 | /// longer active. These are safe to remove. 25 | pub fn find_dangling_references<'repo>( 26 | repo: &'repo Repo, 27 | event_replayer: &EventReplayer, 28 | event_cursor: EventCursor, 29 | ) -> eyre::Result>> { 30 | let mut result = Vec::new(); 31 | for reference in repo.get_all_references()? { 32 | let reference_name = reference.get_name()?; 33 | if !is_gc_ref(&reference_name) { 34 | continue; 35 | } 36 | 37 | // The graph only contains commits, so we don't need to handle the 38 | // case of the reference not peeling to a valid commit. (It might be 39 | // a reference to a different kind of object.) 40 | let commit = match reference.peel_to_commit()? { 41 | Some(commit) => commit, 42 | None => continue, 43 | }; 44 | 45 | match event_replayer.get_cursor_commit_activity_status(event_cursor, commit.get_oid()) { 46 | CommitActivityStatus::Active => { 47 | // Do nothing. 48 | } 49 | CommitActivityStatus::Inactive => { 50 | // This commit hasn't been observed, but it's possible that the user expected it 51 | // to remain. Do nothing. See https://github.com/arxanas/git-branchless/issues/412. 52 | } 53 | CommitActivityStatus::Obsolete => { 54 | // This commit was explicitly hidden by some operation. 55 | result.push(reference) 56 | } 57 | } 58 | } 59 | Ok(result) 60 | } 61 | 62 | /// Mark a commit as reachable. 63 | /// 64 | /// Once marked as reachable, the commit won't be collected by Git's garbage 65 | /// collection mechanism until first garbage-collected by branchless itself 66 | /// (using the `gc` function). 67 | /// 68 | /// If the commit does not exist (such as if it was already garbage-collected), then this is a no-op. 69 | /// 70 | /// Args: 71 | /// * `repo`: The Git repository. 72 | /// * `commit_oid`: The commit OID to mark as reachable. 73 | #[instrument] 74 | pub fn mark_commit_reachable(repo: &Repo, commit_oid: NonZeroOid) -> eyre::Result<()> { 75 | let ref_name = format!("refs/branchless/{commit_oid}"); 76 | eyre::ensure!( 77 | Reference::is_valid_name(&ref_name), 78 | format!("Invalid ref name to mark commit as reachable: {ref_name}") 79 | ); 80 | 81 | // NB: checking for the commit first with `find_commit` is racy, as the `create_reference` call 82 | // could still fail if the commit is deleted by then, but it's too hard to propagate whether the 83 | // commit was not found from `create_reference`. 84 | if repo.find_commit(commit_oid)?.is_some() { 85 | repo.create_reference( 86 | &ref_name.into(), 87 | commit_oid, 88 | true, 89 | "branchless: marking commit as reachable", 90 | ) 91 | .wrap_err("Creating reference")?; 92 | } 93 | 94 | Ok(()) 95 | } 96 | 97 | /// Run branchless's garbage collection. 98 | /// 99 | /// Frees any references to commits which are no longer visible in the smartlog. 100 | #[instrument] 101 | pub fn gc(effects: &Effects) -> eyre::Result<()> { 102 | let repo = Repo::from_current_dir()?; 103 | let conn = repo.get_db_conn()?; 104 | let event_log_db = EventLogDb::new(&conn)?; 105 | let event_replayer = EventReplayer::from_event_log_db(effects, &repo, &event_log_db)?; 106 | let event_cursor = event_replayer.make_default_cursor(); 107 | 108 | writeln!( 109 | effects.get_output_stream(), 110 | "branchless: collecting garbage" 111 | )?; 112 | let dangling_references = find_dangling_references(&repo, &event_replayer, event_cursor)?; 113 | let num_dangling_references = Pluralize { 114 | determiner: None, 115 | amount: dangling_references.len(), 116 | unit: ("dangling reference", "dangling references"), 117 | } 118 | .to_string(); 119 | for mut reference in dangling_references.into_iter() { 120 | reference.delete()?; 121 | } 122 | 123 | writeln!( 124 | effects.get_output_stream(), 125 | "branchless: {num_dangling_references} deleted", 126 | )?; 127 | Ok(()) 128 | } 129 | -------------------------------------------------------------------------------- /git-branchless/tests/test_eventlog.rs: -------------------------------------------------------------------------------- 1 | use lib::core::effects::Effects; 2 | use lib::core::eventlog::testing::{get_event_replayer_events, redact_event_timestamp}; 3 | use lib::core::eventlog::{Event, EventLogDb, EventReplayer}; 4 | use lib::core::formatting::Glyphs; 5 | use lib::testing::make_git; 6 | 7 | #[test] 8 | fn test_git_v2_31_events() -> eyre::Result<()> { 9 | let git = make_git()?; 10 | 11 | if !git.supports_reference_transactions()? || git.produces_auto_merge_refs()? { 12 | return Ok(()); 13 | } 14 | 15 | git.init_repo()?; 16 | git.run(&["checkout", "-b", "test1"])?; 17 | git.commit_file("test1", 1)?; 18 | git.run(&["checkout", "HEAD^"])?; 19 | git.commit_file("test2", 2)?; 20 | git.branchless("hide", &["test1"])?; 21 | 22 | let effects = Effects::new_suppress_for_test(Glyphs::text()); 23 | let repo = git.get_repo()?; 24 | let conn = repo.get_db_conn()?; 25 | let event_log_db = EventLogDb::new(&conn)?; 26 | let event_replayer = EventReplayer::from_event_log_db(&effects, &repo, &event_log_db)?; 27 | let events: Vec = get_event_replayer_events(&event_replayer) 28 | .iter() 29 | .cloned() 30 | .map(redact_event_timestamp) 31 | .collect(); 32 | insta::assert_debug_snapshot!(events, @r###" 33 | [ 34 | RefUpdateEvent { 35 | timestamp: 0.0, 36 | event_tx_id: Id( 37 | 1, 38 | ), 39 | ref_name: ReferenceName( 40 | "refs/heads/test1", 41 | ), 42 | old_oid: 0000000000000000000000000000000000000000, 43 | new_oid: f777ecc9b0db5ed372b2615695191a8a17f79f24, 44 | message: None, 45 | }, 46 | RefUpdateEvent { 47 | timestamp: 0.0, 48 | event_tx_id: Id( 49 | 2, 50 | ), 51 | ref_name: ReferenceName( 52 | "HEAD", 53 | ), 54 | old_oid: f777ecc9b0db5ed372b2615695191a8a17f79f24, 55 | new_oid: f777ecc9b0db5ed372b2615695191a8a17f79f24, 56 | message: None, 57 | }, 58 | RefUpdateEvent { 59 | timestamp: 0.0, 60 | event_tx_id: Id( 61 | 3, 62 | ), 63 | ref_name: ReferenceName( 64 | "HEAD", 65 | ), 66 | old_oid: f777ecc9b0db5ed372b2615695191a8a17f79f24, 67 | new_oid: 62fc20d2a290daea0d52bdc2ed2ad4be6491010e, 68 | message: None, 69 | }, 70 | RefUpdateEvent { 71 | timestamp: 0.0, 72 | event_tx_id: Id( 73 | 3, 74 | ), 75 | ref_name: ReferenceName( 76 | "refs/heads/test1", 77 | ), 78 | old_oid: f777ecc9b0db5ed372b2615695191a8a17f79f24, 79 | new_oid: 62fc20d2a290daea0d52bdc2ed2ad4be6491010e, 80 | message: None, 81 | }, 82 | CommitEvent { 83 | timestamp: 0.0, 84 | event_tx_id: Id( 85 | 4, 86 | ), 87 | commit_oid: NonZeroOid(62fc20d2a290daea0d52bdc2ed2ad4be6491010e), 88 | }, 89 | RefUpdateEvent { 90 | timestamp: 0.0, 91 | event_tx_id: Id( 92 | 5, 93 | ), 94 | ref_name: ReferenceName( 95 | "HEAD", 96 | ), 97 | old_oid: 0000000000000000000000000000000000000000, 98 | new_oid: f777ecc9b0db5ed372b2615695191a8a17f79f24, 99 | message: None, 100 | }, 101 | RefUpdateEvent { 102 | timestamp: 0.0, 103 | event_tx_id: Id( 104 | 6, 105 | ), 106 | ref_name: ReferenceName( 107 | "HEAD", 108 | ), 109 | old_oid: 62fc20d2a290daea0d52bdc2ed2ad4be6491010e, 110 | new_oid: f777ecc9b0db5ed372b2615695191a8a17f79f24, 111 | message: None, 112 | }, 113 | RefUpdateEvent { 114 | timestamp: 0.0, 115 | event_tx_id: Id( 116 | 7, 117 | ), 118 | ref_name: ReferenceName( 119 | "HEAD", 120 | ), 121 | old_oid: f777ecc9b0db5ed372b2615695191a8a17f79f24, 122 | new_oid: fe65c1fe15584744e649b2c79d4cf9b0d878f92e, 123 | message: None, 124 | }, 125 | CommitEvent { 126 | timestamp: 0.0, 127 | event_tx_id: Id( 128 | 8, 129 | ), 130 | commit_oid: NonZeroOid(fe65c1fe15584744e649b2c79d4cf9b0d878f92e), 131 | }, 132 | ObsoleteEvent { 133 | timestamp: 0.0, 134 | event_tx_id: Id( 135 | 9, 136 | ), 137 | commit_oid: NonZeroOid(62fc20d2a290daea0d52bdc2ed2ad4be6491010e), 138 | }, 139 | RefUpdateEvent { 140 | timestamp: 0.0, 141 | event_tx_id: Id( 142 | 9, 143 | ), 144 | ref_name: ReferenceName( 145 | "refs/heads/test1", 146 | ), 147 | old_oid: 62fc20d2a290daea0d52bdc2ed2ad4be6491010e, 148 | new_oid: 0000000000000000000000000000000000000000, 149 | message: None, 150 | }, 151 | ] 152 | "###); 153 | 154 | Ok(()) 155 | } 156 | -------------------------------------------------------------------------------- /git-branchless-test/src/worker.rs: -------------------------------------------------------------------------------- 1 | use std::collections::{HashSet, VecDeque}; 2 | use std::fmt::Debug; 3 | use std::hash::Hash; 4 | use std::sync::{Arc, Condvar, Mutex}; 5 | 6 | use crossbeam::channel::Sender; 7 | use lib::core::effects::ProgressHandle; 8 | use tracing::{debug, warn}; 9 | 10 | pub(crate) type WorkerId = usize; 11 | 12 | pub trait Job: Clone + Debug + Eq + Hash {} 13 | impl Job for T {} 14 | 15 | #[derive(Debug)] 16 | pub(crate) enum JobResult { 17 | Done(J, Output), 18 | Error(WorkerId, J, String), 19 | } 20 | 21 | #[derive(Debug)] 22 | struct WorkQueueState { 23 | jobs: VecDeque, 24 | accepted_jobs: HashSet, 25 | is_active: bool, 26 | } 27 | 28 | impl Default for WorkQueueState { 29 | fn default() -> Self { 30 | Self { 31 | jobs: Default::default(), 32 | accepted_jobs: Default::default(), 33 | is_active: true, 34 | } 35 | } 36 | } 37 | 38 | #[derive(Clone, Debug)] 39 | pub(crate) struct WorkQueue { 40 | state: Arc>>, 41 | cond_var: Arc, 42 | } 43 | 44 | impl WorkQueue { 45 | pub fn new() -> Self { 46 | Self { 47 | state: Default::default(), 48 | cond_var: Default::default(), 49 | } 50 | } 51 | 52 | pub fn set(&self, jobs: Vec) { 53 | let mut state = self.state.lock().unwrap(); 54 | state.jobs = jobs 55 | .into_iter() 56 | .filter(|job| !state.accepted_jobs.contains(job)) 57 | .collect(); 58 | self.cond_var.notify_all(); 59 | } 60 | 61 | pub fn close(&self) { 62 | let mut state = self.state.lock().unwrap(); 63 | state.jobs.clear(); 64 | state.is_active = false; 65 | self.cond_var.notify_all(); 66 | } 67 | 68 | pub fn pop_blocking(&self) -> Option { 69 | enum WakeupCond { 70 | Inactive, 71 | NewJob, 72 | } 73 | fn wakeup_cond(state: &WorkQueueState) -> Option { 74 | if !state.is_active { 75 | Some(WakeupCond::Inactive) 76 | } else if !state.jobs.is_empty() { 77 | Some(WakeupCond::NewJob) 78 | } else { 79 | None 80 | } 81 | } 82 | 83 | let mut state = self.state.lock().unwrap(); 84 | loop { 85 | match wakeup_cond(&state) { 86 | Some(WakeupCond::Inactive) => break None, 87 | Some(WakeupCond::NewJob) => { 88 | let job = state 89 | .jobs 90 | .pop_front() 91 | .expect("Condition variable should have ensured that jobs is non-empty"); 92 | if !state.accepted_jobs.insert(job.clone()) { 93 | warn!(?job, "Job was already accepted"); 94 | } 95 | break Some(job); 96 | } 97 | None => { 98 | state = self 99 | .cond_var 100 | .wait_while(state, |state| wakeup_cond(state).is_none()) 101 | .unwrap(); 102 | } 103 | } 104 | } 105 | } 106 | } 107 | 108 | pub(crate) fn worker( 109 | progress: &ProgressHandle, 110 | worker_id: WorkerId, 111 | work_queue: WorkQueue, 112 | result_tx: Sender>, 113 | setup: impl Fn() -> eyre::Result, 114 | f: impl Fn(J, &Context) -> eyre::Result, 115 | ) { 116 | debug!(?worker_id, "Worker spawned"); 117 | 118 | let context = match setup() { 119 | Ok(context) => context, 120 | Err(err) => { 121 | panic!("Worker {worker_id} could not open repository at: {err}"); 122 | } 123 | }; 124 | 125 | let run_job = |job: J| -> eyre::Result { 126 | let test_output = f(job.clone(), &context)?; 127 | progress.notify_progress_inc(1); 128 | 129 | debug!(?worker_id, ?job, "Worker sending Done job result"); 130 | let should_terminate = result_tx 131 | .send(JobResult::Done(job.clone(), test_output)) 132 | .is_err(); 133 | debug!( 134 | ?worker_id, 135 | ?job, 136 | ?should_terminate, 137 | "Worker finished sending Done job result" 138 | ); 139 | Ok(should_terminate) 140 | }; 141 | 142 | while let Some(job) = work_queue.pop_blocking() { 143 | debug!(?worker_id, ?job, "Worker accepted job"); 144 | let job_result = run_job(job.clone()); 145 | debug!(?worker_id, ?job, "Worker finished job"); 146 | match job_result { 147 | Ok(true) => break, 148 | Ok(false) => { 149 | // Continue. 150 | } 151 | Err(err) => { 152 | debug!(?worker_id, ?job, "Worker sending Error job result"); 153 | result_tx 154 | .send(JobResult::Error(worker_id, job.clone(), err.to_string())) 155 | .ok(); 156 | debug!(?worker_id, ?job, "Worker sending Error job result"); 157 | } 158 | } 159 | } 160 | debug!(?worker_id, "Worker exiting"); 161 | } 162 | -------------------------------------------------------------------------------- /git-branchless-lib/src/git/index.rs: -------------------------------------------------------------------------------- 1 | use std::path::{Path, PathBuf}; 2 | 3 | use eyre::Context; 4 | use tracing::instrument; 5 | 6 | use crate::core::eventlog::EventTransactionId; 7 | 8 | use super::{FileMode, GitRunInfo, GitRunOpts, GitRunResult, MaybeZeroOid, NonZeroOid, Repo, Tree}; 9 | 10 | /// The possible stages for items in the index. 11 | #[derive(Copy, Clone, Debug)] 12 | pub enum Stage { 13 | /// Normal staged change. 14 | Stage0, 15 | 16 | /// For a merge conflict, the contents of the file at the common ancestor of the merged commits. 17 | Stage1, 18 | 19 | /// "Our" changes. 20 | Stage2, 21 | 22 | /// "Their" changes (from the commit being merged in). 23 | Stage3, 24 | } 25 | 26 | impl Stage { 27 | pub(super) fn get_trailer(&self) -> &'static str { 28 | match self { 29 | Stage::Stage0 => "Branchless-stage-0", 30 | Stage::Stage1 => "Branchless-stage-1", 31 | Stage::Stage2 => "Branchless-stage-2", 32 | Stage::Stage3 => "Branchless-stage-3", 33 | } 34 | } 35 | } 36 | 37 | impl From for i32 { 38 | fn from(stage: Stage) -> Self { 39 | match stage { 40 | Stage::Stage0 => 0, 41 | Stage::Stage1 => 1, 42 | Stage::Stage2 => 2, 43 | Stage::Stage3 => 3, 44 | } 45 | } 46 | } 47 | 48 | /// An entry in the Git index. 49 | #[derive(Clone, Debug, PartialEq, Eq, Hash)] 50 | pub struct IndexEntry { 51 | pub(super) oid: MaybeZeroOid, 52 | pub(super) file_mode: FileMode, 53 | } 54 | 55 | /// The Git index. 56 | pub struct Index { 57 | pub(super) inner: git2::Index, 58 | } 59 | 60 | impl std::fmt::Debug for Index { 61 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 62 | write!(f, "") 63 | } 64 | } 65 | 66 | impl Index { 67 | /// Whether or not there are unresolved merge conflicts in the index. 68 | pub fn has_conflicts(&self) -> bool { 69 | self.inner.has_conflicts() 70 | } 71 | 72 | /// Get the (stage 0) entry for the given path. 73 | pub fn get_entry(&self, path: &Path) -> Option { 74 | self.get_entry_in_stage(path, Stage::Stage0) 75 | } 76 | 77 | /// Get the entry for the given path in the given stage. 78 | pub fn get_entry_in_stage(&self, path: &Path, stage: Stage) -> Option { 79 | self.inner 80 | .get_path(path, i32::from(stage)) 81 | .map(|entry| IndexEntry { 82 | oid: entry.id.into(), 83 | file_mode: { 84 | // `libgit2` uses u32 for file modes in index entries, but 85 | // i32 for file modes in tree entries for some reason. 86 | let mode = i32::try_from(entry.mode).unwrap(); 87 | FileMode::from(mode) 88 | }, 89 | }) 90 | } 91 | 92 | /// Update the index from the given tree and write it to disk. 93 | pub fn update_from_tree(&mut self, tree: &Tree) -> eyre::Result<()> { 94 | self.inner.read_tree(&tree.inner)?; 95 | self.inner.write().wrap_err("writing index") 96 | } 97 | } 98 | 99 | /// The command to update the index, as defined by `git update-index`. 100 | #[allow(missing_docs)] 101 | #[derive(Clone, Debug)] 102 | pub enum UpdateIndexCommand { 103 | Delete { 104 | path: PathBuf, 105 | }, 106 | Update { 107 | path: PathBuf, 108 | stage: Stage, 109 | mode: FileMode, 110 | oid: NonZeroOid, 111 | }, 112 | } 113 | 114 | /// Update the index. This handles updates to stages other than 0. 115 | /// 116 | /// libgit2 doesn't offer a good way of updating the index for higher stages, so 117 | /// internally we use `git update-index` directly. 118 | #[instrument] 119 | pub fn update_index( 120 | git_run_info: &GitRunInfo, 121 | repo: &Repo, 122 | index: &Index, 123 | event_tx_id: EventTransactionId, 124 | commands: &[UpdateIndexCommand], 125 | ) -> eyre::Result<()> { 126 | let stdin = { 127 | let mut buf = Vec::new(); 128 | for command in commands { 129 | use std::io::Write; 130 | 131 | match command { 132 | UpdateIndexCommand::Delete { path } => { 133 | write!( 134 | &mut buf, 135 | "0 {zero} 0\t{path}\0", 136 | zero = MaybeZeroOid::Zero, 137 | path = path.display(), 138 | )?; 139 | } 140 | 141 | UpdateIndexCommand::Update { 142 | path, 143 | stage, 144 | mode, 145 | oid, 146 | } => { 147 | write!( 148 | &mut buf, 149 | "{mode} {sha1} {stage}\t{path}\0", 150 | sha1 = oid, 151 | stage = i32::from(*stage), 152 | path = path.display(), 153 | )?; 154 | } 155 | } 156 | } 157 | buf 158 | }; 159 | 160 | let GitRunResult { .. } = git_run_info 161 | .run_silent( 162 | repo, 163 | Some(event_tx_id), 164 | &["update-index", "-z", "--index-info"], 165 | GitRunOpts { 166 | treat_git_failure_as_error: true, 167 | stdin: Some(stdin), 168 | }, 169 | ) 170 | .wrap_err("Updating index")?; 171 | Ok(()) 172 | } 173 | -------------------------------------------------------------------------------- /git-branchless-reword/src/dialoguer_edit.rs: -------------------------------------------------------------------------------- 1 | //! Fork of `dialoguer::edit`. 2 | //! 3 | //! Originally from 4 | //! 5 | //! There are bugs we want to fix and behaviors we want to customize, and their 6 | //! release schedule may not align with ours. This chunk of code is fairly 7 | //! small, so we can vendor it here. 8 | //! 9 | //! `dialoguer` is originally released under the MIT license: 10 | //! 11 | //! The MIT License (MIT) 12 | //! Copyright (c) 2017 Armin Ronacher 13 | //! 14 | //! Permission is hereby granted, free of charge, to any person obtaining a copy 15 | //! of this software and associated documentation files (the "Software"), to deal 16 | //! in the Software without restriction, including without limitation the rights 17 | //! to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 18 | //! copies of the Software, and to permit persons to whom the Software is 19 | //! furnished to do so, subject to the following conditions: 20 | //! 21 | //! The above copyright notice and this permission notice shall be included in all 22 | //! copies or substantial portions of the Software. 23 | //! 24 | //! THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 25 | //! IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 26 | //! FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 27 | //! AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 28 | //! LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 29 | //! OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 30 | //! SOFTWARE. 31 | 32 | use std::env; 33 | use std::ffi::{OsStr, OsString}; 34 | use std::fs; 35 | use std::io::{self, Read, Write}; 36 | use std::process; 37 | 38 | /// Launches the default editor to edit a string. 39 | /// 40 | /// ## Example 41 | /// 42 | /// ```rust,no_run 43 | /// use git_branchless_reword::dialoguer_edit::Editor; 44 | /// 45 | /// if let Some(rv) = Editor::new().edit("Enter a commit message").unwrap() { 46 | /// println!("Your message:"); 47 | /// println!("{}", rv); 48 | /// } else { 49 | /// println!("Abort!"); 50 | /// } 51 | /// ``` 52 | pub struct Editor { 53 | editor: OsString, 54 | extension: String, 55 | require_save: bool, 56 | trim_newlines: bool, 57 | } 58 | 59 | fn get_default_editor() -> OsString { 60 | if let Some(prog) = env::var_os("VISUAL") { 61 | return prog; 62 | } 63 | if let Some(prog) = env::var_os("EDITOR") { 64 | return prog; 65 | } 66 | if cfg!(windows) { 67 | "notepad.exe".into() 68 | } else { 69 | "vi".into() 70 | } 71 | } 72 | 73 | impl Default for Editor { 74 | fn default() -> Self { 75 | Self::new() 76 | } 77 | } 78 | 79 | impl Editor { 80 | /// Creates a new editor. 81 | pub fn new() -> Self { 82 | Self { 83 | editor: get_default_editor(), 84 | extension: ".txt".into(), 85 | require_save: true, 86 | trim_newlines: true, 87 | } 88 | } 89 | 90 | /// Sets a specific editor executable. 91 | pub fn executable>(&mut self, val: S) -> &mut Self { 92 | self.editor = val.as_ref().into(); 93 | self 94 | } 95 | 96 | /// Sets a specific extension 97 | pub fn extension(&mut self, val: &str) -> &mut Self { 98 | self.extension = val.into(); 99 | self 100 | } 101 | 102 | /// Enables or disables the save requirement. 103 | pub fn require_save(&mut self, val: bool) -> &mut Self { 104 | self.require_save = val; 105 | self 106 | } 107 | 108 | /// Enables or disables trailing newline stripping. 109 | /// 110 | /// This is on by default. 111 | pub fn trim_newlines(&mut self, val: bool) -> &mut Self { 112 | self.trim_newlines = val; 113 | self 114 | } 115 | 116 | /// Launches the editor to edit a string. 117 | /// 118 | /// Returns `None` if the file was not saved or otherwise the 119 | /// entered text. 120 | pub fn edit(&self, s: &str) -> io::Result> { 121 | let mut f = tempfile::Builder::new() 122 | .prefix("COMMIT_EDITMSG-") 123 | .suffix(&self.extension) 124 | .rand_bytes(12) 125 | .tempfile()?; 126 | f.write_all(s.as_bytes())?; 127 | f.flush()?; 128 | let ts = fs::metadata(f.path())?.modified()?; 129 | 130 | let s: String = self.editor.clone().into_string().unwrap(); 131 | let (cmd, args) = match shell_words::split(&s) { 132 | Ok(mut parts) => { 133 | let cmd = parts.remove(0); 134 | (cmd, parts) 135 | } 136 | Err(_) => (s, vec![]), 137 | }; 138 | 139 | let rv = process::Command::new(cmd) 140 | .args(args) 141 | .arg(f.path()) 142 | .spawn()? 143 | .wait()?; 144 | 145 | if rv.success() && self.require_save && ts >= fs::metadata(f.path())?.modified()? { 146 | return Ok(None); 147 | } 148 | 149 | let mut new_f = fs::File::open(f.path())?; 150 | let mut rv = String::new(); 151 | new_f.read_to_string(&mut rv)?; 152 | 153 | if self.trim_newlines { 154 | let len = rv.trim_end_matches(&['\n', '\r'][..]).len(); 155 | rv.truncate(len); 156 | } 157 | 158 | Ok(Some(rv)) 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /git-branchless/tests/test_branchless.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use itertools::Itertools; 4 | use lib::testing::{make_git, GitRunOptions}; 5 | 6 | #[test] 7 | fn test_commands() -> eyre::Result<()> { 8 | let git = make_git()?; 9 | 10 | git.init_repo()?; 11 | git.commit_file("test", 1)?; 12 | git.detach_head()?; 13 | git.commit_file("test2", 2)?; 14 | 15 | { 16 | let stdout = git.smartlog()?; 17 | insta::assert_snapshot!(stdout, @r###" 18 | : 19 | O 3df4b93 (master) create test.txt 20 | | 21 | @ 73b746c create test2.txt 22 | "###); 23 | } 24 | 25 | { 26 | let (stdout, _stderr) = git.branchless("hide", &["HEAD"])?; 27 | insta::assert_snapshot!(stdout, @r###" 28 | Hid commit: 73b746c create test2.txt 29 | To unhide this 1 commit, run: git undo 30 | "###); 31 | } 32 | 33 | { 34 | let (stdout, _stderr) = git.branchless("unhide", &["HEAD"])?; 35 | insta::assert_snapshot!(stdout, @r###" 36 | Unhid commit: 73b746c create test2.txt 37 | To hide this 1 commit, run: git undo 38 | "###); 39 | } 40 | 41 | { 42 | let (stdout, _stderr) = git.branchless("prev", &[])?; 43 | insta::assert_snapshot!(stdout, @r###" 44 | branchless: running command: checkout master 45 | : 46 | @ 3df4b93 (> master) create test.txt 47 | | 48 | o 73b746c create test2.txt 49 | "###); 50 | } 51 | 52 | { 53 | let (stdout, _stderr) = git.branchless("next", &[])?; 54 | insta::assert_snapshot!(stdout, @r###" 55 | branchless: running command: checkout 73b746ca864a21fc0c3dedbc937eaa9e279b73eb 56 | : 57 | O 3df4b93 (master) create test.txt 58 | | 59 | @ 73b746c create test2.txt 60 | "###); 61 | } 62 | 63 | Ok(()) 64 | } 65 | 66 | #[test] 67 | fn test_profiling() -> eyre::Result<()> { 68 | let git = make_git()?; 69 | git.init_repo()?; 70 | 71 | git.branchless_with_options( 72 | "smartlog", 73 | &[], 74 | &GitRunOptions { 75 | env: { 76 | let mut env: HashMap = HashMap::new(); 77 | env.insert("RUST_PROFILE".to_string(), "1".to_string()); 78 | env 79 | }, 80 | ..Default::default() 81 | }, 82 | )?; 83 | 84 | let entries: Vec<_> = std::fs::read_dir(&git.repo_path)?.try_collect()?; 85 | assert!(entries 86 | .iter() 87 | .any(|entry| entry.file_name().to_str().unwrap().contains("trace-"))); 88 | 89 | Ok(()) 90 | } 91 | 92 | #[test] 93 | fn test_sparse_checkout() -> eyre::Result<()> { 94 | let git = make_git()?; 95 | git.init_repo()?; 96 | 97 | if git.run(&["sparse-checkout", "set"]).is_err() { 98 | return Ok(()); 99 | } 100 | 101 | { 102 | let (stdout, _stderr) = git.run(&["config", "extensions.worktreeConfig"])?; 103 | insta::assert_snapshot!(stdout, @r###" 104 | true 105 | "###); 106 | } 107 | 108 | if let Ok(stdout) = git.smartlog() { 109 | insta::assert_snapshot!(stdout, @"@ f777ecc (> master) create initial.txt 110 | "); 111 | } else { 112 | let (stdout, _stderr) = git.branchless_with_options( 113 | "smartlog", 114 | &[], 115 | &GitRunOptions { 116 | expected_exit_code: 1, 117 | ..Default::default() 118 | }, 119 | )?; 120 | insta::assert_snapshot!(stdout, @r###" 121 | Error: the Git configuration setting `extensions.worktreeConfig` is enabled in 122 | this repository. Due to upstream libgit2 limitations, git-branchless does not 123 | support repositories with this configuration option enabled. 124 | 125 | Usually, this configuration setting is enabled when initializing a sparse 126 | checkout. See https://github.com/arxanas/git-branchless/issues/278 for more 127 | information. 128 | 129 | Here are some options: 130 | 131 | - To unset the configuration option, run: git config --unset extensions.worktreeConfig 132 | - This is safe unless you created another worktree also using a sparse checkout. 133 | - Try upgrading to Git v2.36+ and reinitializing your sparse checkout. 134 | "###); 135 | } 136 | 137 | Ok(()) 138 | } 139 | 140 | /// The Git index v4 format is supported as of libgit2 v1.8.0: https://github.com/arxanas/git-branchless/issues/894#issuecomment-2044059209 141 | /// libgit2 v1.8.0 was bundled into git2 v0.19.0: https://github.com/arxanas/git-branchless/issues/894#issuecomment-2270760735 142 | /// 143 | /// See https://github.com/arxanas/git-branchless/issues/894 144 | /// See https://github.com/arxanas/git-branchless/issues/1363 145 | #[test] 146 | fn test_index_version_4() -> eyre::Result<()> { 147 | let git = make_git()?; 148 | git.init_repo()?; 149 | 150 | git.run(&["update-index", "--index-version=4"])?; 151 | { 152 | let stdout = git.smartlog()?; 153 | insta::assert_snapshot!(stdout, @r###" 154 | @ f777ecc (> master) create initial.txt 155 | "###); 156 | } 157 | 158 | { 159 | let (stdout, _stderr) = git.branchless("switch", &["HEAD"])?; 160 | insta::assert_snapshot!(stdout, @r###" 161 | branchless: running command: checkout HEAD 162 | @ f777ecc (> master) create initial.txt 163 | "###); 164 | } 165 | 166 | Ok(()) 167 | } 168 | -------------------------------------------------------------------------------- /git-branchless-lib/tests/test_eventlog.rs: -------------------------------------------------------------------------------- 1 | use std::str::FromStr; 2 | 3 | use branchless::core::eventlog::testing::{new_event_cursor, new_event_transaction_id}; 4 | use branchless::core::eventlog::{ 5 | testing::new_event_replayer, Event, EventLogDb, EventTransactionId, 6 | }; 7 | use branchless::git::{MaybeZeroOid, NonZeroOid, ReferenceName}; 8 | use branchless::testing::make_git; 9 | 10 | #[test] 11 | fn test_drop_non_meaningful_events() -> eyre::Result<()> { 12 | let event_tx_id = new_event_transaction_id(123); 13 | let meaningful_event = Event::CommitEvent { 14 | timestamp: 0.0, 15 | event_tx_id, 16 | commit_oid: NonZeroOid::from_str("abc")?, 17 | }; 18 | let mut replayer = new_event_replayer("refs/heads/master".into()); 19 | replayer.process_event(&meaningful_event); 20 | replayer.process_event(&Event::RefUpdateEvent { 21 | timestamp: 0.0, 22 | event_tx_id, 23 | ref_name: ReferenceName::from("ORIG_HEAD"), 24 | old_oid: MaybeZeroOid::from_str("abc")?, 25 | new_oid: MaybeZeroOid::from_str("def")?, 26 | message: None, 27 | }); 28 | replayer.process_event(&Event::RefUpdateEvent { 29 | timestamp: 0.0, 30 | event_tx_id, 31 | ref_name: ReferenceName::from("CHERRY_PICK_HEAD"), 32 | old_oid: MaybeZeroOid::Zero, 33 | new_oid: MaybeZeroOid::Zero, 34 | message: None, 35 | }); 36 | 37 | let cursor = replayer.make_default_cursor(); 38 | assert_eq!( 39 | replayer.get_event_before_cursor(cursor), 40 | Some((1, &meaningful_event)) 41 | ); 42 | Ok(()) 43 | } 44 | 45 | #[test] 46 | fn test_different_event_transaction_ids() -> eyre::Result<()> { 47 | let git = make_git()?; 48 | 49 | if git.produces_auto_merge_refs()? { 50 | return Ok(()); 51 | } 52 | 53 | git.init_repo()?; 54 | git.commit_file("test1", 1)?; 55 | git.branchless("hide", &["--no-delete-branches", "HEAD"])?; 56 | 57 | let repo = git.get_repo()?; 58 | let conn = repo.get_db_conn()?; 59 | let event_log_db = EventLogDb::new(&conn)?; 60 | let events = event_log_db.get_events()?; 61 | let event_tx_ids: Vec = 62 | events.iter().map(|event| event.get_event_tx_id()).collect(); 63 | if git.supports_reference_transactions()? { 64 | insta::assert_debug_snapshot!(event_tx_ids, @r###" 65 | [ 66 | Id( 67 | 1, 68 | ), 69 | Id( 70 | 1, 71 | ), 72 | Id( 73 | 2, 74 | ), 75 | Id( 76 | 3, 77 | ), 78 | ] 79 | "###); 80 | } else { 81 | insta::assert_debug_snapshot!(event_tx_ids, @r###" 82 | [ 83 | Id( 84 | 1, 85 | ), 86 | Id( 87 | 2, 88 | ), 89 | ] 90 | "###); 91 | } 92 | Ok(()) 93 | } 94 | 95 | #[test] 96 | fn test_advance_cursor_by_transaction() -> eyre::Result<()> { 97 | let mut event_replayer = new_event_replayer("refs/heads/master".into()); 98 | for (timestamp, event_tx_id) in (0..).zip(&[1, 1, 2, 2, 3, 4]) { 99 | let timestamp = f64::from(timestamp); 100 | event_replayer.process_event(&Event::UnobsoleteEvent { 101 | timestamp, 102 | event_tx_id: new_event_transaction_id(*event_tx_id), 103 | commit_oid: NonZeroOid::from_str("abc")?, 104 | }); 105 | } 106 | 107 | assert_eq!( 108 | event_replayer.advance_cursor_by_transaction(new_event_cursor(0), 1), 109 | new_event_cursor(2), 110 | ); 111 | assert_eq!( 112 | event_replayer.advance_cursor_by_transaction(new_event_cursor(1), 1), 113 | new_event_cursor(4), 114 | ); 115 | assert_eq!( 116 | event_replayer.advance_cursor_by_transaction(new_event_cursor(2), 1), 117 | new_event_cursor(4), 118 | ); 119 | assert_eq!( 120 | event_replayer.advance_cursor_by_transaction(new_event_cursor(3), 1), 121 | new_event_cursor(5), 122 | ); 123 | assert_eq!( 124 | event_replayer.advance_cursor_by_transaction(new_event_cursor(4), 1), 125 | new_event_cursor(5), 126 | ); 127 | assert_eq!( 128 | event_replayer.advance_cursor_by_transaction(new_event_cursor(5), 1), 129 | new_event_cursor(6), 130 | ); 131 | assert_eq!( 132 | event_replayer.advance_cursor_by_transaction(new_event_cursor(6), 1), 133 | new_event_cursor(6), 134 | ); 135 | 136 | assert_eq!( 137 | event_replayer.advance_cursor_by_transaction(new_event_cursor(6), -1), 138 | new_event_cursor(5), 139 | ); 140 | assert_eq!( 141 | event_replayer.advance_cursor_by_transaction(new_event_cursor(5), -1), 142 | new_event_cursor(4), 143 | ); 144 | assert_eq!( 145 | event_replayer.advance_cursor_by_transaction(new_event_cursor(4), -1), 146 | new_event_cursor(2), 147 | ); 148 | assert_eq!( 149 | event_replayer.advance_cursor_by_transaction(new_event_cursor(3), -1), 150 | new_event_cursor(2), 151 | ); 152 | assert_eq!( 153 | event_replayer.advance_cursor_by_transaction(new_event_cursor(2), -1), 154 | new_event_cursor(0), 155 | ); 156 | assert_eq!( 157 | event_replayer.advance_cursor_by_transaction(new_event_cursor(1), -1), 158 | new_event_cursor(0), 159 | ); 160 | assert_eq!( 161 | event_replayer.advance_cursor_by_transaction(new_event_cursor(0), -1), 162 | new_event_cursor(0), 163 | ); 164 | 165 | Ok(()) 166 | } 167 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | me@waleedkhan.name. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /git-branchless/tests/test_bug_report.rs: -------------------------------------------------------------------------------- 1 | use lazy_static::lazy_static; 2 | use lib::testing::make_git; 3 | use regex::Regex; 4 | 5 | lazy_static! { 6 | static ref TIMESTAMP_RE: Regex = Regex::new("timestamp: ([0-9.]+)").unwrap(); 7 | } 8 | 9 | fn redact_timestamp(str: String) -> String { 10 | TIMESTAMP_RE 11 | .replace_all(&str, "timestamp: ") 12 | .to_string() 13 | } 14 | 15 | #[test] 16 | fn test_bug_report() -> eyre::Result<()> { 17 | let git = make_git()?; 18 | 19 | if !git.supports_reference_transactions()? || git.produces_auto_merge_refs()? { 20 | return Ok(()); 21 | } 22 | git.init_repo()?; 23 | 24 | git.commit_file("test1", 1)?; 25 | git.commit_file("test2", 2)?; 26 | 27 | { 28 | let (stdout, _stderr) = git.branchless("bug-report", &[])?; 29 | let stdout = redact_timestamp(stdout); 30 | 31 | // Exclude the platform-specific information for this test. 32 | let stdout = match stdout.split_once("#### Hooks") { 33 | Some((_, stdout)) => stdout, 34 | None => &stdout, 35 | }; 36 | let stdout = stdout.trim(); 37 | 38 | insta::assert_snapshot!(stdout, @r###" 39 | Hooks directory: `/.git/hooks` 40 | 41 |
42 | Show 7 hooks 43 | 44 | ##### Hook `post-applypatch` 45 | 46 | ``` 47 | #!/bin/sh 48 | ## START BRANCHLESS CONFIG 49 | 50 | git branchless hook post-applypatch "$@" 51 | 52 | ## END BRANCHLESS CONFIG 53 | ``` 54 | ##### Hook `post-checkout` 55 | 56 | ``` 57 | #!/bin/sh 58 | ## START BRANCHLESS CONFIG 59 | 60 | git branchless hook post-checkout "$@" 61 | 62 | ## END BRANCHLESS CONFIG 63 | ``` 64 | ##### Hook `post-commit` 65 | 66 | ``` 67 | #!/bin/sh 68 | ## START BRANCHLESS CONFIG 69 | 70 | git branchless hook post-commit "$@" 71 | 72 | ## END BRANCHLESS CONFIG 73 | ``` 74 | ##### Hook `post-merge` 75 | 76 | ``` 77 | #!/bin/sh 78 | ## START BRANCHLESS CONFIG 79 | 80 | git branchless hook post-merge "$@" 81 | 82 | ## END BRANCHLESS CONFIG 83 | ``` 84 | ##### Hook `post-rewrite` 85 | 86 | ``` 87 | #!/bin/sh 88 | ## START BRANCHLESS CONFIG 89 | 90 | git branchless hook post-rewrite "$@" 91 | 92 | ## END BRANCHLESS CONFIG 93 | ``` 94 | ##### Hook `pre-auto-gc` 95 | 96 | ``` 97 | #!/bin/sh 98 | ## START BRANCHLESS CONFIG 99 | 100 | git branchless hook pre-auto-gc "$@" 101 | 102 | ## END BRANCHLESS CONFIG 103 | ``` 104 | ##### Hook `reference-transaction` 105 | 106 | ``` 107 | #!/bin/sh 108 | ## START BRANCHLESS CONFIG 109 | 110 | # Avoid canceling the reference transaction in the case that `branchless` fails 111 | # for whatever reason. 112 | git branchless hook reference-transaction "$@" || ( 113 | echo 'branchless: Failed to process reference transaction!' 114 | echo 'branchless: Some events (e.g. branch updates) may have been lost.' 115 | echo 'branchless: This is a bug. Please report it.' 116 | ) 117 | 118 | ## END BRANCHLESS CONFIG 119 | ``` 120 | 121 |
122 | 123 | #### Events 124 | 125 | 126 |
127 | Show 5 events 128 | 129 | ##### Event ID: 6, transaction ID: 4 (message: post-commit) 130 | 131 | 1. `CommitEvent { timestamp: , event_tx_id: Id(4), commit_oid: NonZeroOid(96d1c37a3d4363611c49f7e52186e189a04c531f) }` 132 | ``` 133 | : 134 | @ 96d1c37 (> master) xxxxxx xxxxxxxxx 135 | ``` 136 | ##### Event ID: 4, transaction ID: 3 (message: reference-transaction) 137 | 138 | 1. `RefUpdateEvent { timestamp: , event_tx_id: Id(3), ref_name: ReferenceName("HEAD"), old_oid: 62fc20d2a290daea0d52bdc2ed2ad4be6491010e, new_oid: 96d1c37a3d4363611c49f7e52186e189a04c531f, message: None }` 139 | 1. `RefUpdateEvent { timestamp: , event_tx_id: Id(3), ref_name: ReferenceName("refs/heads/master"), old_oid: 62fc20d2a290daea0d52bdc2ed2ad4be6491010e, new_oid: 96d1c37a3d4363611c49f7e52186e189a04c531f, message: None }` 140 | ``` 141 | : 142 | @ 96d1c37 (> master) xxxxxx xxxxxxxxx 143 | ``` 144 | ##### Event ID: 3, transaction ID: 2 (message: post-commit) 145 | 146 | 1. `CommitEvent { timestamp: , event_tx_id: Id(2), commit_oid: NonZeroOid(62fc20d2a290daea0d52bdc2ed2ad4be6491010e) }` 147 | ``` 148 | : 149 | @ 96d1c37 (> master) xxxxxx xxxxxxxxx 150 | ``` 151 | ##### Event ID: 1, transaction ID: 1 (message: reference-transaction) 152 | 153 | 1. `RefUpdateEvent { timestamp: , event_tx_id: Id(1), ref_name: ReferenceName("HEAD"), old_oid: f777ecc9b0db5ed372b2615695191a8a17f79f24, new_oid: 62fc20d2a290daea0d52bdc2ed2ad4be6491010e, message: None }` 154 | 1. `RefUpdateEvent { timestamp: , event_tx_id: Id(1), ref_name: ReferenceName("refs/heads/master"), old_oid: f777ecc9b0db5ed372b2615695191a8a17f79f24, new_oid: 62fc20d2a290daea0d52bdc2ed2ad4be6491010e, message: None }` 155 | ``` 156 | : 157 | @ 96d1c37 (> master) xxxxxx xxxxxxxxx 158 | ``` 159 | There are no previous available events. 160 | ``` 161 | : 162 | @ 96d1c37 (> master) xxxxxx xxxxxxxxx 163 | ``` 164 | 165 |
166 | "###); 167 | } 168 | 169 | Ok(()) 170 | } 171 | --------------------------------------------------------------------------------