diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 52aea9f231..d10f29b013 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -2,8 +2,6 @@ name: build on: push: - branches: - - main pull_request: permissions: read-all diff --git a/.github/workflows/scorecards.yml b/.github/workflows/scorecards.yml index 6eab471ddb..1627bccde7 100644 --- a/.github/workflows/scorecards.yml +++ b/.github/workflows/scorecards.yml @@ -46,6 +46,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard. - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@23acc5c183826b7a8a97bce3cecc52db901f8251 + uses: github/codeql-action/upload-sarif@b611370bb5703a7efb587f9d136a52ea24c5c38c with: sarif_file: results.sarif diff --git a/CHANGELOG.md b/CHANGELOG.md index ae543a8daa..8cecb229d8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,10 +19,30 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * `jj fix` now defaults to the broader revset `-s reachable(@, mutable())` instead of `-s @`. +* Dropped support for deprecated `jj branch delete`/`forget` `--glob` option. + +* `jj branch set` now creates new branch if it doesn't exist. Use `jj branch + move` to ensure that the target branch already exists. + [#3584](https://github.com/martinvonz/jj/issues/3584) + ### Deprecations +* Replacing `-l` shorthand for `--limit` with `-n` in `jj log`, `jj op log` and `jj obslog`. + +* `jj split --siblings` is deprecated in favor of `jj split --parallel` (to + match `jj parallelize`). + +* `jj file show` replaces `jj cat`. + +* `jj file chmod` replaces `jj chmod`. + +* `jj file list` replaces `jj files`. + ### New features +* Support background filesystem monitoring via watchman triggers enabled with + the `core.watchman.register_snapshot_trigger = true` config. + * Show paths to config files when configuration errors occur * `jj fix` now supports configuring the default revset for `-s` using the @@ -31,8 +51,33 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * The `descendants()` revset function now accepts an optional `depth` argument; like the `ancestors()` depth argument, it limits the depth of the set. +* Revset/template aliases now support function overloading. + [#2966](https://github.com/martinvonz/jj/issues/2966) + +* Conflicted files are individually simplified before being materialized. + +* `jj file` now groups commands for working with files. + +* New command `jj branch move` let you update branches by name pattern or source + revision. + +* New diff option `jj diff --name-only` allows for easier shell scripting. + +* `jj git push -c ` can now accept revsets that resolve to multiple + revisions. This means that `jj git push -c xyz -c abc` is now equivalent to + `jj git push -c 'all:(xyz | abc)'`. + +* `jj prev` and `jj next` have gained a `--conflict` flag which moves you + to the next conflict in a child commit. + +* New command `jj git remote set-url` that sets the url of a git remote. + ### Fixed bugs +* `jj git push` now ignores immutable commits when checking whether a + to-be-pushed commit has conflicts, or has no description / committer / author + set. [#3029](https://github.com/martinvonz/jj/issues/3029) + ## [0.18.0] - 2024-06-05 ### Breaking changes @@ -64,6 +109,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 were global flags and specifying them once would insert the new commit before/ after all the specified commits. + ### Deprecations * Attempting to alias a built-in command now gives a warning, rather than being diff --git a/Cargo.lock b/Cargo.lock index 4e0c50902f..a15525ddde 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,9 +4,9 @@ version = 3 [[package]] name = "addr2line" -version = "0.21.0" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" +checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678" dependencies = [ "gimli", ] @@ -40,9 +40,9 @@ dependencies = [ [[package]] name = "allocator-api2" -version = "0.2.16" +version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5" +checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" [[package]] name = "android-tzdata" @@ -67,47 +67,48 @@ checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" [[package]] name = "anstream" -version = "0.6.13" +version = "0.6.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d96bd03f33fe50a863e394ee9718a706f988b9079b20c3784fb726e7678b62fb" +checksum = "418c75fa768af9c03be99d17643f93f79bbba589895012a80e3452a19ddda15b" dependencies = [ "anstyle", "anstyle-parse", "anstyle-query", "anstyle-wincon", "colorchoice", + "is_terminal_polyfill", "utf8parse", ] [[package]] name = "anstyle" -version = "1.0.6" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8901269c6307e8d93993578286ac0edf7f195079ffff5ebdeea6a59ffb7e36bc" +checksum = "038dfcf04a5feb68e9c60b21c9625a54c2c0616e79b72b0fd87075a056ae1d1b" [[package]] name = "anstyle-parse" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c" +checksum = "c03a11a9034d92058ceb6ee011ce58af4a9bf61491aa7e1e59ecd24bd40d22d4" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.0.2" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648" +checksum = "ad186efb764318d35165f1758e7dcef3b10628e26d41a44bc5550652e6804391" dependencies = [ "windows-sys 0.52.0", ] [[package]] name = "anstyle-wincon" -version = "3.0.2" +version = "3.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7" +checksum = "61a38449feb7068f52bb06c12759005cf459ee52bb4adc1d5a7c4322d716fb19" dependencies = [ "anstyle", "windows-sys 0.52.0", @@ -165,9 +166,9 @@ dependencies = [ [[package]] name = "autocfg" -version = "1.1.0" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" [[package]] name = "backoff" @@ -182,9 +183,9 @@ dependencies = [ [[package]] name = "backtrace" -version = "0.3.71" +version = "0.3.73" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26b05800d2e817c8b3b4b54abd461726265fa9789ae34330622f2db9ee696f9d" +checksum = "5cc23269a4f8976d0a4d2e7109211a419fe30e8d88d677cd60b6bc79c5732e0a" dependencies = [ "addr2line", "cc", @@ -232,15 +233,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05efc5cfd9110c8416e471df0e96702d58690178e206e61b7173706673c93706" dependencies = [ "memchr", - "regex-automata 0.4.6", + "regex-automata 0.4.7", "serde", ] [[package]] name = "bumpalo" -version = "3.15.4" +version = "3.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ff69b9dd49fd426c69a0db9fc04dd934cdb6645ff000864d98f7e2af8830eaa" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" [[package]] name = "byteorder" @@ -248,16 +249,6 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" -[[package]] -name = "bytes" -version = "0.4.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "206fdffcfa2df7cbe15601ef46c813fce0965eb3286db6b56c583b814b51c81c" -dependencies = [ - "byteorder", - "iovec", -] - [[package]] name = "bytes" version = "1.6.0" @@ -269,9 +260,9 @@ dependencies = [ [[package]] name = "camino" -version = "1.1.6" +version = "1.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c59e92b5a388f549b863a7bea62612c09f24c8393560709a54558a9abdfb3b9c" +checksum = "e0ec6b951b160caa93cc0c7b209e5a3bff7aae9062213451ac99493cd844c239" dependencies = [ "serde", ] @@ -322,12 +313,13 @@ dependencies = [ [[package]] name = "cc" -version = "1.0.90" +version = "1.0.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8cd6604a82acf3039f1144f54b8eb34e91ffba622051189e71b781822d5ee1f5" +checksum = "96c51067fd44124faa7f870b4b1c969379ad32b2ba805aa959430ceaa384f695" dependencies = [ "jobserver", "libc", + "once_cell", ] [[package]] @@ -345,7 +337,7 @@ dependencies = [ "android-tzdata", "iana-time-zone", "num-traits", - "windows-targets 0.52.4", + "windows-targets 0.52.5", ] [[package]] @@ -387,9 +379,9 @@ dependencies = [ [[package]] name = "clap-markdown" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "325f50228f76921784b6d9f2d62de6778d834483248eefecd27279174797e579" +checksum = "8ebc67e6266e14f8b31541c2f204724fa2ac7ad5c17d6f5908fbb92a60f42cff" dependencies = [ "clap", ] @@ -409,9 +401,9 @@ dependencies = [ [[package]] name = "clap_complete" -version = "4.5.5" +version = "4.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2020fa13af48afc65a9a87335bda648309ab3d154cd03c7ff95b378c7ed39c4" +checksum = "fbca90c87c2a04da41e95d1856e8bcd22f159bdbfa147314d2ce5218057b0e58" dependencies = [ "clap", ] @@ -432,7 +424,7 @@ version = "4.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c780290ccf4fb26629baa7a1081e68ced113f1d3ec302fa5948f1c381ebf06c6" dependencies = [ - "heck 0.5.0", + "heck", "proc-macro2", "quote", "syn", @@ -440,15 +432,15 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.7.0" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce" +checksum = "4b82cf0babdbd58558212896d1a4272303a57bdb245c2bf1147185fb45640e70" [[package]] name = "clap_mangen" -version = "0.2.20" +version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1dd95b5ebb5c1c54581dd6346f3ed6a79a3eef95dd372fc2ac13d535535300e" +checksum = "74b70fc13e60c0e1d490dc50eb73a749be6d81f4ef03783df1d9b7b0c62bc937" dependencies = [ "clap", "roff", @@ -456,15 +448,15 @@ dependencies = [ [[package]] name = "clru" -version = "0.6.1" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8191fa7302e03607ff0e237d4246cc043ff5b3cb9409d995172ba3bea16b807" +checksum = "cbd0f76e066e64fdc5631e3bb46381254deab9ef1158292f27c8c57e3bf3fe59" [[package]] name = "colorchoice" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" +checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422" [[package]] name = "compact_str" @@ -522,9 +514,9 @@ dependencies = [ [[package]] name = "crc32fast" -version = "1.4.0" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3855a8a784b474f333699ef2bbca9db2c4a1f6d9088a90a2d25b1eb53111eaa" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" dependencies = [ "cfg-if", ] @@ -580,9 +572,9 @@ dependencies = [ [[package]] name = "crossbeam-channel" -version = "0.5.12" +version = "0.5.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab3db02a9c5b5121e1e42fbdb1aeb65f5e02624cc58c43f2884c6ccac0b82f95" +checksum = "33480d6946193aa8033910124896ca395333cae7e2d1113d1fef6c3272217df2" dependencies = [ "crossbeam-utils", ] @@ -617,9 +609,9 @@ dependencies = [ [[package]] name = "crossbeam-utils" -version = "0.8.19" +version = "0.8.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" +checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" [[package]] name = "crossterm" @@ -729,9 +721,9 @@ checksum = "56ce8c6da7551ec6c462cbaf3bfbc75131ebbfa1c944aeaa9dab51ca1c5f0c3b" [[package]] name = "either" -version = "1.12.0" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3dca9240753cf90908d7e4aac30f630662b02aebaa1b58a3cadabdb23385b58b" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" [[package]] name = "encode_unicode" @@ -747,9 +739,9 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "errno" -version = "0.3.8" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" +checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" dependencies = [ "libc", "windows-sys 0.52.0", @@ -785,7 +777,7 @@ checksum = "1ee447700ac8aa0b2f2bd7bc4462ad686ba06baa6727ac149a2d6277f0d240fd" dependencies = [ "cfg-if", "libc", - "redox_syscall", + "redox_syscall 0.4.1", "windows-sys 0.52.0", ] @@ -797,9 +789,9 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" [[package]] name = "flate2" -version = "1.0.28" +version = "1.0.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46303f565772937ffe1d394a4fac6f411c6013172fadde9dcdb1e147a086940e" +checksum = "5f54427cfd1c7829e2a139fcefea601bf088ebca651d2bf53ebc600eac295dae" dependencies = [ "crc32fast", "miniz_oxide", @@ -935,9 +927,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.12" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", "libc", @@ -946,9 +938,9 @@ dependencies = [ [[package]] name = "gimli" -version = "0.28.1" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" +checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" [[package]] name = "git2" @@ -1017,7 +1009,7 @@ dependencies = [ "gix-utils", "itoa", "thiserror", - "winnow 0.6.5", + "winnow 0.6.13", ] [[package]] @@ -1070,7 +1062,7 @@ dependencies = [ "smallvec", "thiserror", "unicode-bom", - "winnow 0.6.5", + "winnow 0.6.13", ] [[package]] @@ -1150,9 +1142,9 @@ dependencies = [ [[package]] name = "gix-fs" -version = "0.11.0" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f78f7d6dcda7a5809efd73a33b145e3dce7421c460df21f32126f9732736b0c" +checksum = "c3338ff92a2164f5209f185ec0cd316f571a72676bb01d27e22f2867ba69f77a" dependencies = [ "fastrand", "gix-features", @@ -1161,9 +1153,9 @@ dependencies = [ [[package]] name = "gix-glob" -version = "0.16.2" +version = "0.16.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "682bdc43cb3c00dbedfcc366de2a849b582efd8d886215dbad2ea662ec156bb5" +checksum = "c2a29ad0990cf02c48a7aac76ed0dbddeb5a0d070034b83675cc3bbf937eace4" dependencies = [ "bitflags 2.5.0", "bstr", @@ -1258,7 +1250,7 @@ dependencies = [ "itoa", "smallvec", "thiserror", - "winnow 0.6.5", + "winnow 0.6.13", ] [[package]] @@ -1328,9 +1320,9 @@ dependencies = [ [[package]] name = "gix-ref" -version = "0.44.0" +version = "0.44.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b36752b448647acd59c9668fdd830b16d07db1e6d9c3b3af105c1605a6e23d9" +checksum = "3394a2997e5bc6b22ebc1e1a87b41eeefbcfcff3dbfa7c4bd73cb0ac8f1f3e2e" dependencies = [ "gix-actor", "gix-date", @@ -1345,7 +1337,7 @@ dependencies = [ "gix-validate", "memmap2", "thiserror", - "winnow 0.6.5", + "winnow 0.6.13", ] [[package]] @@ -1490,15 +1482,15 @@ dependencies = [ "aho-corasick", "bstr", "log", - "regex-automata 0.4.6", - "regex-syntax 0.8.2", + "regex-automata 0.4.7", + "regex-syntax 0.8.4", ] [[package]] name = "half" -version = "2.4.0" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5eceaaeec696539ddaf7b333340f1af35a5aa87ae3e4f3ead0532f72affab2e" +checksum = "6dd08c532ae367adf81c312a4580bc67f1d0fe8bc9c460520283f4c0ff277888" dependencies = [ "cfg-if", "crunchy", @@ -1506,20 +1498,14 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.14.3" +version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" dependencies = [ "ahash", "allocator-api2", ] -[[package]] -name = "heck" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" - [[package]] name = "heck" version = "0.5.0" @@ -1590,7 +1576,7 @@ dependencies = [ "globset", "log", "memchr", - "regex-automata 0.4.6", + "regex-automata 0.4.7", "same-file", "walkdir", "winapi-util", @@ -1627,22 +1613,13 @@ dependencies = [ [[package]] name = "instant" -version = "0.1.12" +version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" dependencies = [ "cfg-if", ] -[[package]] -name = "iovec" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2b3ea6ff95e175473f8ffe6a7eb7c00d054240321b84c57051175fe3c1e075e" -dependencies = [ - "libc", -] - [[package]] name = "is-terminal" version = "0.4.12" @@ -1654,6 +1631,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "is_terminal_polyfill" +version = "1.70.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800" + [[package]] name = "itertools" version = "0.10.5" @@ -1674,9 +1657,9 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.10" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" [[package]] name = "jj-cli" @@ -1743,7 +1726,7 @@ dependencies = [ "async-trait", "backoff", "blake2", - "bytes 1.6.0", + "bytes", "chrono", "config", "criterion", @@ -1802,9 +1785,9 @@ dependencies = [ [[package]] name = "jobserver" -version = "0.1.28" +version = "0.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab46a6e9526ddef3ae7f787c06f0f2600639ba80ea3eade3d8e670a2230f51d6" +checksum = "d2b099aaa34a9751c5bf0878add70444e1ed2dd73f347be99003d4577277de6e" dependencies = [ "libc", ] @@ -1856,13 +1839,12 @@ dependencies = [ [[package]] name = "libredox" -version = "0.0.1" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85c833ca1e66078851dba29046874e38f08b2c883700aa29a03ddd3b23814ee8" +checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" dependencies = [ "bitflags 2.5.0", "libc", - "redox_syscall", ] [[package]] @@ -1881,9 +1863,9 @@ dependencies = [ [[package]] name = "libz-sys" -version = "1.1.16" +version = "1.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e143b5e666b2695d28f6bca6497720813f699c9602dd7f5cac91008b8ada7f9" +checksum = "c15da26e5af7e25c90b37a2d75cdbf940cf4a55316de9d84c679c9b8bfabf82e" dependencies = [ "cc", "libc", @@ -1899,15 +1881,15 @@ checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" [[package]] name = "linux-raw-sys" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" +checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" [[package]] name = "lock_api" -version = "0.4.11" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" dependencies = [ "autocfg", "scopeguard", @@ -1945,9 +1927,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.7.1" +version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" [[package]] name = "memmap2" @@ -1966,9 +1948,9 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.7.2" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d811f3e15f28568be3407c8e7fdb6514c1cda3cb30683f15b6a1a1dc4ea14a7" +checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" dependencies = [ "adler", ] @@ -2002,9 +1984,9 @@ dependencies = [ [[package]] name = "multimap" -version = "0.8.3" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5ce46fe64a9d73be07dcbe690a38ce1b293be448fd8ce1e6c1b8062c9f72c6a" +checksum = "defc4c55412d89136f966bbb339008b474350e5e6e78d2714439c386b3137a03" [[package]] name = "nom" @@ -2034,9 +2016,9 @@ checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" [[package]] name = "num-traits" -version = "0.2.18" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", ] @@ -2062,9 +2044,9 @@ dependencies = [ [[package]] name = "object" -version = "0.32.2" +version = "0.36.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" +checksum = "576dfe1fc8f9df304abb159d767a29d0476f7750fbf8aa7ad07816004a207434" dependencies = [ "memchr", ] @@ -2092,18 +2074,18 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-src" -version = "300.2.3+3.2.1" +version = "300.3.1+3.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cff92b6f71555b61bb9315f7c64da3ca43d87531622120fea0195fc761b4843" +checksum = "7259953d42a81bf137fbbd73bd30a8e1914d6dce43c2b90ed575783a22608b91" dependencies = [ "cc", ] [[package]] name = "openssl-sys" -version = "0.9.101" +version = "0.9.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dda2b0f344e78efc2facf7d195d098df0dd72151b26ab98da807afc26c198dff" +checksum = "c597637d56fbc83893a35eb0dd04b2b8e7a50c91e64e9493e398b5df4fb45fa2" dependencies = [ "cc", "libc", @@ -2126,9 +2108,9 @@ checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" [[package]] name = "parking_lot" -version = "0.12.1" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" dependencies = [ "lock_api", "parking_lot_core", @@ -2136,22 +2118,22 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.9" +version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" dependencies = [ "cfg-if", "libc", - "redox_syscall", + "redox_syscall 0.5.2", "smallvec", - "windows-targets 0.48.5", + "windows-targets 0.52.5", ] [[package]] name = "paste" -version = "1.0.14" +version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" [[package]] name = "pathdiff" @@ -2212,9 +2194,9 @@ dependencies = [ [[package]] name = "petgraph" -version = "0.6.4" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1d3afd2628e69da2be385eb6f2fd57c8ac7977ceeff6dc166ff1657b0e386a9" +checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" dependencies = [ "fixedbitset", "indexmap", @@ -2222,9 +2204,9 @@ dependencies = [ [[package]] name = "pin-project-lite" -version = "0.2.13" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" +checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" [[package]] name = "pin-utils" @@ -2240,9 +2222,9 @@ checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" [[package]] name = "plotters" -version = "0.3.5" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2c224ba00d7cadd4d5c660deaf2098e5e80e07846537c51f9cfa4be50c1fd45" +checksum = "a15b6eccb8484002195a3e44fe65a4ce8e93a625797a063735536fd59cb01cf3" dependencies = [ "num-traits", "plotters-backend", @@ -2253,15 +2235,15 @@ dependencies = [ [[package]] name = "plotters-backend" -version = "0.3.5" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e76628b4d3a7581389a35d5b6e2139607ad7c75b17aed325f210aa91f4a9609" +checksum = "414cec62c6634ae900ea1c56128dfe87cf63e7caece0852ec76aba307cebadb7" [[package]] name = "plotters-svg" -version = "0.3.5" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38f6d39893cca0701371e3c27294f09797214b86f1fb951b89ade8ec04e2abab" +checksum = "81b30686a7d9c3e010b84284bdd26a29f2138574f52f5eb6f794fc0ad924e705" dependencies = [ "plotters-backend", ] @@ -2323,9 +2305,9 @@ dependencies = [ [[package]] name = "prettyplease" -version = "0.2.16" +version = "0.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a41cf62165e97c7f814d2221421dbb9afcbcdb0a88068e5ea206e19951c2cbb5" +checksum = "5f12335488a2f3b0a83b14edad48dca9879ce89b2edd10e80237e4e852dd645e" dependencies = [ "proc-macro2", "syn", @@ -2333,9 +2315,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.85" +version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22244ce15aa966053a896d1accb3a6e68469b97c7f33f284b99f0d576879fc23" +checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" dependencies = [ "unicode-ident", ] @@ -2352,7 +2334,7 @@ version = "0.12.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "deb1435c188b76130da55f17a466d252ff7b1418b2ad3e037d127b94e3411f29" dependencies = [ - "bytes 1.6.0", + "bytes", "prost-derive", ] @@ -2362,8 +2344,8 @@ version = "0.12.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22505a5c94da8e3b7c2996394d1c933236c4d743e81a410bcca4e6989fc066a4" dependencies = [ - "bytes 1.6.0", - "heck 0.5.0", + "bytes", + "heck", "itertools 0.12.1", "log", "multimap", @@ -2487,11 +2469,20 @@ dependencies = [ "bitflags 1.3.2", ] +[[package]] +name = "redox_syscall" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c82cf8cff14456045f55ec4241383baeff27af886adb72ffb2162f99911de0fd" +dependencies = [ + "bitflags 2.5.0", +] + [[package]] name = "redox_users" -version = "0.4.4" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a18479200779601e498ada4e8c1e1f50e3ee19deb0259c25825a98b5603b2cb4" +checksum = "bd283d9651eeda4b2a83a43c1c91b266c40fd76ecd39a50a8c630ae69dc72891" dependencies = [ "getrandom", "libredox", @@ -2526,8 +2517,8 @@ checksum = "b91213439dad192326a0d7c6ee3955910425f441d7038e0d6933b0aec5c4517f" dependencies = [ "aho-corasick", "memchr", - "regex-automata 0.4.6", - "regex-syntax 0.8.2", + "regex-automata 0.4.7", + "regex-syntax 0.8.4", ] [[package]] @@ -2541,13 +2532,13 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.6" +version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea" +checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" dependencies = [ "aho-corasick", "memchr", - "regex-syntax 0.8.2", + "regex-syntax 0.8.4", ] [[package]] @@ -2558,9 +2549,9 @@ checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" [[package]] name = "regex-syntax" -version = "0.8.2" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" +checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" [[package]] name = "roff" @@ -2591,9 +2582,9 @@ dependencies = [ [[package]] name = "rustc-demangle" -version = "0.1.23" +version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" [[package]] name = "rustix" @@ -2610,15 +2601,15 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.14" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" +checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" [[package]] name = "ryu" -version = "1.0.17" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" [[package]] name = "same-file" @@ -2654,9 +2645,9 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "semver" -version = "1.0.22" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92d43fe69e652f3df9bdc2b85b2854a0825b86e4fb76bc44d945137d053639ca" +checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" dependencies = [ "serde", ] @@ -2672,17 +2663,27 @@ dependencies = [ [[package]] name = "serde_bser" -version = "0.3.1" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b929ea725591083cbca8b8ea178ed6efc918eccd40b784e199ce88967104199" +checksum = "a56b4bcc15e42e5b5ae16c6f75582bef80d36c6ffe2c03b1b5317754b38f8717" dependencies = [ "anyhow", "byteorder", - "bytes 0.4.12", + "bytes", "serde", + "serde_bytes", "thiserror", ] +[[package]] +name = "serde_bytes" +version = "0.11.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b8497c313fd43ab992087548117643f6fcd935cbf36f176ffda0aacf9591734" +dependencies = [ + "serde", +] + [[package]] name = "serde_derive" version = "1.0.203" @@ -2696,9 +2697,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.117" +version = "1.0.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "455182ea6142b14f93f4bc5320a2b31c1f266b66a4a5c858b013302a5d8cbfc3" +checksum = "d947f6b3163d8857ea16c4fa0dd4840d52f3041039a85decd46867eb1abef2e4" dependencies = [ "itoa", "ryu", @@ -2707,9 +2708,9 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "0.6.5" +version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb3622f419d1296904700073ea6cc23ad690adbd66f13ea683df73298736f0c1" +checksum = "79e674e01f999af37c49f70a6ede167a8a60b2503e56c5599532a65baa5969a0" dependencies = [ "serde", ] @@ -2763,18 +2764,18 @@ dependencies = [ [[package]] name = "signal-hook-registry" -version = "1.4.1" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" +checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" dependencies = [ "libc", ] [[package]] name = "similar" -version = "2.4.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32fea41aca09ee824cc9724996433064c89f7777e60762749a4170a14abbfa21" +checksum = "fa42c91313f1d05da9b26f267f931cf178d4aba455b4c4622dd7355eb80c6640" [[package]] name = "slab" @@ -2799,9 +2800,9 @@ checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c" [[package]] name = "socket2" -version = "0.5.6" +version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05ffd9c0a93b7543e062e759284fcf5f5e3b098501104bfbdde4d404db792871" +checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" dependencies = [ "libc", "windows-sys 0.52.0", @@ -2840,11 +2841,11 @@ dependencies = [ [[package]] name = "strum_macros" -version = "0.26.2" +version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6cf59daf282c0a494ba14fd21610a0325f9f90ec9d1231dea26bcb1d696c946" +checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" dependencies = [ - "heck 0.4.1", + "heck", "proc-macro2", "quote", "rustversion", @@ -2853,15 +2854,15 @@ dependencies = [ [[package]] name = "subtle" -version = "2.5.0" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" -version = "2.0.66" +version = "2.0.68" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c42f3f41a2de00b01c0aaad383c5a45241efc8b2d1eda5661812fda5f3cdcff5" +checksum = "901fa70d88b9d6c98022e23b4136f9f3e54e4662c3bc1bd1d84a42a9a0f0c1e9" dependencies = [ "proc-macro2", "quote", @@ -3055,7 +3056,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba4f4a02a7a80d6f274636f0aa95c7e383b912d41fe721a31f29e29698585a4a" dependencies = [ "backtrace", - "bytes 1.6.0", + "bytes", "libc", "mio", "num_cpus", @@ -3084,7 +3085,7 @@ version = "0.6.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "36943ee01a6d67977dd3f84a5a1d2efeb4ada3a1ae771cadfaa535d9d9fc6507" dependencies = [ - "bytes 1.6.0", + "bytes", "futures-core", "futures-io", "futures-sink", @@ -3105,9 +3106,9 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.6.5" +version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1" +checksum = "4badfd56924ae69bcc9039335b2e017639ce3f9b001c393c1b2d1ef846ce2cbf" dependencies = [ "serde", ] @@ -3211,9 +3212,9 @@ checksum = "ed646292ffc8188ef8ea4d1e0e0150fb15a5c2e12ad9b8fc191ae7a8a7f3c4b9" [[package]] name = "uluru" -version = "3.0.0" +version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "794a32261a1f5eb6a4462c81b59cec87b5c27d5deea7dd1ac8fc781c41d226db" +checksum = "7c8a2469e56e6e5095c82ccd3afb98dad95f7af7929aab6d8ba8d6e0f73657da" dependencies = [ "arrayvec", ] @@ -3275,9 +3276,9 @@ checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d" [[package]] name = "url" -version = "2.5.0" +version = "2.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" +checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c" dependencies = [ "form_urlencoded", "idna", @@ -3286,9 +3287,9 @@ dependencies = [ [[package]] name = "utf8parse" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "valuable" @@ -3395,12 +3396,12 @@ checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" [[package]] name = "watchman_client" -version = "0.8.0" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "839fea2d85719bb69089290d7970bba2131f544448db8f990ea75813c30775ca" +checksum = "88bc4c9bb443a7aae10d4fa7807bffc397805315e2305288c90c80e2f66cfb52" dependencies = [ "anyhow", - "bytes 1.6.0", + "bytes", "futures 0.3.30", "maplit", "serde", @@ -3427,7 +3428,7 @@ version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a44ab49fad634e88f55bf8f9bb3abd2f27d7204172a112c7c9987e01c1c94ea9" dependencies = [ - "redox_syscall", + "redox_syscall 0.4.1", "wasite", "web-sys", ] @@ -3450,11 +3451,11 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" -version = "0.1.6" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" +checksum = "4d4cc384e1e73b93bafa6fb4f1df8c41695c8a91cf9c4c64358067d15a7b6c6b" dependencies = [ - "winapi", + "windows-sys 0.52.0", ] [[package]] @@ -3469,7 +3470,7 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" dependencies = [ - "windows-targets 0.52.4", + "windows-targets 0.52.5", ] [[package]] @@ -3487,7 +3488,7 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets 0.52.4", + "windows-targets 0.52.5", ] [[package]] @@ -3507,17 +3508,18 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.52.4" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dd37b7e5ab9018759f893a1952c9420d060016fc19a472b4bb20d1bdd694d1b" +checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb" dependencies = [ - "windows_aarch64_gnullvm 0.52.4", - "windows_aarch64_msvc 0.52.4", - "windows_i686_gnu 0.52.4", - "windows_i686_msvc 0.52.4", - "windows_x86_64_gnu 0.52.4", - "windows_x86_64_gnullvm 0.52.4", - "windows_x86_64_msvc 0.52.4", + "windows_aarch64_gnullvm 0.52.5", + "windows_aarch64_msvc 0.52.5", + "windows_i686_gnu 0.52.5", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.5", + "windows_x86_64_gnu 0.52.5", + "windows_x86_64_gnullvm 0.52.5", + "windows_x86_64_msvc 0.52.5", ] [[package]] @@ -3528,9 +3530,9 @@ checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" [[package]] name = "windows_aarch64_gnullvm" -version = "0.52.4" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcf46cf4c365c6f2d1cc93ce535f2c8b244591df96ceee75d8e83deb70a9cac9" +checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263" [[package]] name = "windows_aarch64_msvc" @@ -3540,9 +3542,9 @@ checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" [[package]] name = "windows_aarch64_msvc" -version = "0.52.4" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da9f259dd3bcf6990b55bffd094c4f7235817ba4ceebde8e6d11cd0c5633b675" +checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" [[package]] name = "windows_i686_gnu" @@ -3552,9 +3554,15 @@ checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" [[package]] name = "windows_i686_gnu" -version = "0.52.4" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b474d8268f99e0995f25b9f095bc7434632601028cf86590aea5c8a5cb7801d3" +checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9" [[package]] name = "windows_i686_msvc" @@ -3564,9 +3572,9 @@ checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" [[package]] name = "windows_i686_msvc" -version = "0.52.4" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1515e9a29e5bed743cb4415a9ecf5dfca648ce85ee42e15873c3cd8610ff8e02" +checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" [[package]] name = "windows_x86_64_gnu" @@ -3576,9 +3584,9 @@ checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" [[package]] name = "windows_x86_64_gnu" -version = "0.52.4" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5eee091590e89cc02ad514ffe3ead9eb6b660aedca2183455434b93546371a03" +checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9" [[package]] name = "windows_x86_64_gnullvm" @@ -3588,9 +3596,9 @@ checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" [[package]] name = "windows_x86_64_gnullvm" -version = "0.52.4" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77ca79f2451b49fa9e2af39f0747fe999fcda4f5e241b2898624dca97a1f2177" +checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596" [[package]] name = "windows_x86_64_msvc" @@ -3600,9 +3608,9 @@ checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "windows_x86_64_msvc" -version = "0.52.4" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8" +checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" [[package]] name = "winnow" @@ -3615,9 +3623,9 @@ dependencies = [ [[package]] name = "winnow" -version = "0.6.5" +version = "0.6.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dffa400e67ed5a4dd237983829e66475f0a4a26938c4b04c21baede6262215b8" +checksum = "59b5e5f6c299a3c7890b876a2a587f3115162487e704907d9b6cd29473052ba1" dependencies = [ "memchr", ] @@ -3640,18 +3648,18 @@ checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec" [[package]] name = "zerocopy" -version = "0.7.32" +version = "0.7.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74d4d3961e53fa4c9a25a8637fc2bfaf2595b3d3ae34875568a5cf64787716be" +checksum = "ae87e3fcd617500e5d106f0380cf7b77f3c6092aae37191433159dda23cfb087" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.7.32" +version = "0.7.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6" +checksum = "15e934569e47891f7d9411f1a451d947a60e000ab3bd24fbb970f000387d1b3b" dependencies = [ "proc-macro2", "quote", @@ -3679,9 +3687,9 @@ dependencies = [ [[package]] name = "zstd-sys" -version = "2.0.9+zstd.1.5.5" +version = "2.0.11+zstd.1.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e16efa8a874a0481a574084d34cc26fdb3b99627480f785888deb6386506656" +checksum = "75652c55c0b6f3e6f12eb786fe1bc960396bf05a1eb3bf1f3691c3610ac2e6d4" dependencies = [ "cc", "pkg-config", diff --git a/Cargo.toml b/Cargo.toml index a40020d12f..4041e65b7b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,7 +33,7 @@ clap = { version = "4.5.7", features = [ ] } clap_complete = "4.5.5" clap_complete_nushell = "4.5.2" -clap-markdown = "0.1.3" +clap-markdown = "0.1.4" clap_mangen = "0.2.10" chrono = { version = "0.4.38", default-features = false, features = [ "std", @@ -44,7 +44,7 @@ criterion = "0.5.1" crossterm = { version = "0.27", default-features = false } digest = "0.10.7" dirs = "5.0.1" -either = "1.12.0" +either = "1.13.0" esl01-renderdag = "0.3.0" futures = "0.3.30" git2 = "0.18.3" @@ -69,7 +69,7 @@ pest = "2.7.10" pest_derive = "2.7.10" pollster = "0.3.0" pretty_assertions = "1.4.0" -proc-macro2 = "1.0.85" +proc-macro2 = "1.0.86" prost = "0.12.6" prost-build = "0.12.6" quote = "1.0.36" @@ -82,7 +82,7 @@ rpassword = "7.3.1" rustix = { version = "0.38.34", features = ["fs"] } scm-record = "0.3.0" serde = { version = "1.0", features = ["derive"] } -serde_json = "1.0.117" +serde_json = "1.0.118" slab = "0.4.9" smallvec = { version = "1.13.2", features = [ "const_generics", @@ -90,7 +90,7 @@ smallvec = { version = "1.13.2", features = [ "union", ] } strsim = "0.11.1" -syn = "2.0.66" +syn = "2.0.68" tempfile = "3.10.1" test-case = "3.3.1" textwrap = "0.16.1" @@ -108,7 +108,7 @@ tracing-subscriber = { version = "0.3.18", default-features = false, features = ] } unicode-width = "0.1.13" version_check = "0.9.4" -watchman_client = { version = "0.8.0" } +watchman_client = { version = "0.9.0" } whoami = "1.5.1" winreg = "0.52" zstd = "0.12.4" diff --git a/cli/examples/custom-commit-templater/main.rs b/cli/examples/custom-commit-templater/main.rs index 7cab31e7b6..0d9efe4729 100644 --- a/cli/examples/custom-commit-templater/main.rs +++ b/cli/examples/custom-commit-templater/main.rs @@ -30,8 +30,8 @@ use jj_lib::object_id::ObjectId; use jj_lib::repo::Repo; use jj_lib::revset::{ FunctionCallNode, PartialSymbolResolver, RevsetExpression, RevsetFilterExtension, - RevsetFilterExtensionWrapper, RevsetFilterPredicate, RevsetParseContext, RevsetParseError, - RevsetResolutionError, SymbolResolverExtension, + RevsetFilterPredicate, RevsetParseContext, RevsetParseError, RevsetResolutionError, + SymbolResolverExtension, }; use once_cell::sync::OnceCell; @@ -185,7 +185,7 @@ fn even_digits( ) -> Result, RevsetParseError> { function.expect_no_arguments()?; Ok(RevsetExpression::filter(RevsetFilterPredicate::Extension( - RevsetFilterExtensionWrapper(Rc::new(EvenDigitsFilter)), + Rc::new(EvenDigitsFilter), ))) } diff --git a/cli/src/cli_util.rs b/cli/src/cli_util.rs index 18c44c33c1..e4474047d4 100644 --- a/cli/src/cli_util.rs +++ b/cli/src/cli_util.rs @@ -1112,6 +1112,11 @@ impl WorkspaceCommandHelper { Ok(()) }; } + + // Not using self.id_prefix_context() because the disambiguation data + // must not be calculated and cached against arbitrary repo. It's also + // unlikely that the immutable expression contains short hashes. + let id_prefix_context = IdPrefixContext::new(self.revset_extensions.clone()); let to_rewrite_revset = RevsetExpression::commits(commits.into_iter().cloned().collect_vec()); let immutable = revset_util::parse_immutable_expression(&self.revset_parse_context()) @@ -1121,7 +1126,7 @@ impl WorkspaceCommandHelper { let mut expression = RevsetExpressionEvaluator::new( repo, self.revset_extensions.clone(), - self.id_prefix_context()?, + &id_prefix_context, immutable, ); expression.intersect_with(&to_rewrite_revset); @@ -1222,7 +1227,7 @@ See https://github.com/martinvonz/jj/blob/main/docs/working-copy.md#stale-workin let progress = crate::progress::snapshot_progress(ui); let new_tree_id = locked_ws.locked_wc().snapshot(SnapshotOptions { base_ignores, - fsmonitor_kind: self.settings.fsmonitor_kind()?, + fsmonitor_settings: self.settings.fsmonitor_settings()?, progress: progress.as_ref().map(|x| x as _), max_new_file_size: self.settings.max_new_file_size()?, })?; @@ -1408,9 +1413,8 @@ See https://github.com/martinvonz/jj/blob/main/docs/working-copy.md#stale-workin // are millions of commits added to the repo, assuming the revset engine can // efficiently skip non-conflicting commits. Filter out empty commits mostly so // `jj new ` doesn't result in a message about new conflicts. - let conflicts = RevsetExpression::filter(RevsetFilterPredicate::HasConflict).intersection( - &RevsetExpression::filter(RevsetFilterPredicate::File(FilesetExpression::all())), - ); + let conflicts = RevsetExpression::filter(RevsetFilterPredicate::HasConflict) + .filtered(RevsetFilterPredicate::File(FilesetExpression::all())); let removed_conflicts_expr = new_heads.range(&old_heads).intersection(&conflicts); let added_conflicts_expr = old_heads.range(&new_heads).intersection(&conflicts); @@ -1845,6 +1849,7 @@ pub fn print_conflicted_paths( .map(|p| format!("{:width$}", p, width = max_path_len.min(32) + 3)); for ((_, conflict), formatted_path) in std::iter::zip(conflicts.iter(), formatted_paths) { + let conflict = conflict.clone().simplify(); let sides = conflict.num_sides(); let n_adds = conflict.adds().flatten().count(); let deletions = sides - n_adds; @@ -1941,15 +1946,13 @@ pub fn print_checkout_stats( working copy.", stats.skipped_files )?; - if let Some(mut writer) = ui.hint_default() { - writeln!( - writer, - "Inspect the changes compared to the intended target with `jj diff --from {}`. + writeln!( + ui.hint_default(), + "Inspect the changes compared to the intended target with `jj diff --from {}`. Discard the conflicting changes with `jj restore --from {}`.", - short_commit_hash(new_commit.id()), - short_commit_hash(new_commit.id()) - )?; - } + short_commit_hash(new_commit.id()), + short_commit_hash(new_commit.id()) + )?; } Ok(()) } @@ -1997,21 +2000,17 @@ pub fn print_trackable_remote_branches(ui: &Ui, view: &View) -> io::Result<()> { return Ok(()); } - if let Some(mut writer) = ui.hint_default() { + if let Some(mut formatter) = ui.status_formatter() { writeln!( - writer, + formatter.labeled("hint").with_heading("Hint: "), "The following remote branches aren't associated with the existing local branches:" )?; - } - if let Some(mut formatter) = ui.status_formatter() { for full_name in &remote_branch_names { write!(formatter, " ")?; writeln!(formatter.labeled("branch"), "{full_name}")?; } - } - if let Some(mut writer) = ui.hint_default() { writeln!( - writer, + formatter.labeled("hint").with_heading("Hint: "), "Run `jj branch track {names}` to keep local branches updated on future pulls.", names = remote_branch_names.join(" "), )?; @@ -2397,8 +2396,8 @@ pub struct EarlyArgs { pub color: Option, /// Silence non-primary command output /// - /// For example, `jj files` will still list files, but it won't tell you if - /// the working copy was snapshotted or if descendants were rebased. + /// For example, `jj file list ` will still list files, but it won't tell + /// you if the working copy was snapshotted or if descendants were rebased. /// /// Warnings and errors will still be printed. #[arg(long, global = true, action = ArgAction::SetTrue)] @@ -2491,14 +2490,14 @@ fn resolve_default_command( if matches.subcommand_name().is_none() { let args = get_string_or_array(config, "ui.default-command").optional()?; if args.is_none() { - if let Some(mut writer) = ui.hint_default() { - writeln!(writer, "Use `jj -h` for a list of available commands.")?; - writeln!( - writer, - "Run `jj config set --user ui.default-command log` to disable this \ - message." - )?; - } + writeln!( + ui.hint_default(), + "Use `jj -h` for a list of available commands." + )?; + writeln!( + ui.hint_no_heading(), + "Run `jj config set --user ui.default-command log` to disable this message." + )?; } let default_command = args.unwrap_or_else(|| vec!["log".to_string()]); diff --git a/cli/src/commands/branch.rs b/cli/src/commands/branch.rs deleted file mode 100644 index 84974057fa..0000000000 --- a/cli/src/commands/branch.rs +++ /dev/null @@ -1,791 +0,0 @@ -// Copyright 2020-2023 The Jujutsu Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// https://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use std::collections::{HashMap, HashSet}; -use std::fmt; -use std::io::Write as _; - -use clap::builder::NonEmptyStringValueParser; -use itertools::Itertools; -use jj_lib::git; -use jj_lib::object_id::ObjectId; -use jj_lib::op_store::{RefTarget, RemoteRef}; -use jj_lib::repo::Repo; -use jj_lib::revset::RevsetExpression; -use jj_lib::str_util::StringPattern; -use jj_lib::view::View; - -use crate::cli_util::{CommandHelper, RemoteBranchName, RemoteBranchNamePattern, RevisionArg}; -use crate::command_error::{user_error, user_error_with_hint, CommandError}; -use crate::commit_templater::{CommitTemplateLanguage, RefName}; -use crate::ui::Ui; - -/// Manage branches. -/// -/// For information about branches, see -/// https://github.com/martinvonz/jj/blob/main/docs/branches.md. -#[derive(clap::Subcommand, Clone, Debug)] -pub enum BranchCommand { - #[command(visible_alias("c"))] - Create(BranchCreateArgs), - #[command(visible_alias("d"))] - Delete(BranchDeleteArgs), - #[command(visible_alias("f"))] - Forget(BranchForgetArgs), - #[command(visible_alias("l"))] - List(BranchListArgs), - #[command(visible_alias("r"))] - Rename(BranchRenameArgs), - #[command(visible_alias("s"))] - Set(BranchSetArgs), - #[command(visible_alias("t"))] - Track(BranchTrackArgs), - Untrack(BranchUntrackArgs), -} - -/// Create a new branch. -#[derive(clap::Args, Clone, Debug)] -pub struct BranchCreateArgs { - /// The branch's target revision. - #[arg(long, short)] - revision: Option, - - /// The branches to create. - #[arg(required = true, value_parser=NonEmptyStringValueParser::new())] - names: Vec, -} - -/// Delete an existing branch and propagate the deletion to remotes on the -/// next push. -#[derive(clap::Args, Clone, Debug)] -pub struct BranchDeleteArgs { - /// The branches to delete - /// - /// By default, the specified name matches exactly. Use `glob:` prefix to - /// select branches by wildcard pattern. For details, see - /// https://github.com/martinvonz/jj/blob/main/docs/revsets.md#string-patterns. - #[arg(required_unless_present_any(&["glob"]), value_parser = StringPattern::parse)] - pub names: Vec, - - /// Deprecated. Please prefix the pattern with `glob:` instead. - #[arg(long, hide = true, value_parser = StringPattern::glob)] - pub glob: Vec, -} - -/// List branches and their targets -/// -/// By default, a tracking remote branch will be included only if its target is -/// different from the local target. A non-tracking remote branch won't be -/// listed. For a conflicted branch (both local and remote), old target -/// revisions are preceded by a "-" and new target revisions are preceded by a -/// "+". -/// -/// For information about branches, see -/// https://github.com/martinvonz/jj/blob/main/docs/branches.md. -#[derive(clap::Args, Clone, Debug)] -pub struct BranchListArgs { - /// Show all tracking and non-tracking remote branches including the ones - /// whose targets are synchronized with the local branches. - #[arg(long, short, alias = "all")] - all_remotes: bool, - - /// Show remote tracked branches only. Omits local Git-tracking branches by - /// default. - #[arg(long, short, conflicts_with_all = ["all_remotes"])] - tracked: bool, - - /// Show conflicted branches only. - #[arg(long, short, conflicts_with_all = ["all_remotes"])] - conflicted: bool, - - /// Show branches whose local name matches - /// - /// By default, the specified name matches exactly. Use `glob:` prefix to - /// select branches by wildcard pattern. For details, see - /// https://github.com/martinvonz/jj/blob/main/docs/revsets.md#string-patterns. - #[arg(value_parser = StringPattern::parse)] - pub names: Vec, - - /// Show branches whose local targets are in the given revisions. - /// - /// Note that `-r deleted_branch` will not work since `deleted_branch` - /// wouldn't have a local target. - #[arg(long, short)] - revisions: Vec, - - /// Render each branch using the given template - /// - /// All 0-argument methods of the `RefName` type are available as keywords. - /// - /// For the syntax, see https://github.com/martinvonz/jj/blob/main/docs/templates.md - #[arg(long, short = 'T')] - template: Option, -} - -/// Forget everything about a branch, including its local and remote -/// targets. -/// -/// A forgotten branch will not impact remotes on future pushes. It will be -/// recreated on future pulls if it still exists in the remote. -#[derive(clap::Args, Clone, Debug)] -pub struct BranchForgetArgs { - /// The branches to forget - /// - /// By default, the specified name matches exactly. Use `glob:` prefix to - /// select branches by wildcard pattern. For details, see - /// https://github.com/martinvonz/jj/blob/main/docs/revsets.md#string-patterns. - #[arg(required_unless_present_any(&["glob"]), value_parser = StringPattern::parse)] - pub names: Vec, - - /// Deprecated. Please prefix the pattern with `glob:` instead. - #[arg(long, hide = true, value_parser = StringPattern::glob)] - pub glob: Vec, -} - -/// Rename `old` branch name to `new` branch name. -/// -/// The new branch name points at the same commit as the old -/// branch name. -#[derive(clap::Args, Clone, Debug)] -pub struct BranchRenameArgs { - /// The old name of the branch. - pub old: String, - - /// The new name of the branch. - pub new: String, -} - -/// Update an existing branch to point to a certain commit. -#[derive(clap::Args, Clone, Debug)] -pub struct BranchSetArgs { - /// The branch's target revision. - #[arg(long, short)] - pub revision: Option, - - /// Allow moving the branch backwards or sideways. - #[arg(long, short = 'B')] - pub allow_backwards: bool, - - /// The branches to update. - #[arg(required = true)] - pub names: Vec, -} - -/// Start tracking given remote branches -/// -/// A tracking remote branch will be imported as a local branch of the same -/// name. Changes to it will propagate to the existing local branch on future -/// pulls. -#[derive(clap::Args, Clone, Debug)] -pub struct BranchTrackArgs { - /// Remote branches to track - /// - /// By default, the specified name matches exactly. Use `glob:` prefix to - /// select branches by wildcard pattern. For details, see - /// https://github.com/martinvonz/jj/blob/main/docs/revsets.md#string-patterns. - /// - /// Examples: branch@remote, glob:main@*, glob:jjfan-*@upstream - #[arg(required = true, value_name = "BRANCH@REMOTE")] - pub names: Vec, -} - -/// Stop tracking given remote branches -/// -/// A non-tracking remote branch is just a pointer to the last-fetched remote -/// branch. It won't be imported as a local branch on future pulls. -#[derive(clap::Args, Clone, Debug)] -pub struct BranchUntrackArgs { - /// Remote branches to untrack - /// - /// By default, the specified name matches exactly. Use `glob:` prefix to - /// select branches by wildcard pattern. For details, see - /// https://github.com/martinvonz/jj/blob/main/docs/revsets.md#string-patterns. - /// - /// Examples: branch@remote, glob:main@*, glob:jjfan-*@upstream - #[arg(required = true, value_name = "BRANCH@REMOTE")] - pub names: Vec, -} - -fn make_branch_term(branch_names: &[impl fmt::Display]) -> String { - match branch_names { - [branch_name] => format!("branch {}", branch_name), - branch_names => format!("branches {}", branch_names.iter().join(", ")), - } -} - -pub fn cmd_branch( - ui: &mut Ui, - command: &CommandHelper, - subcommand: &BranchCommand, -) -> Result<(), CommandError> { - match subcommand { - BranchCommand::Create(sub_args) => cmd_branch_create(ui, command, sub_args), - BranchCommand::Rename(sub_args) => cmd_branch_rename(ui, command, sub_args), - BranchCommand::Set(sub_args) => cmd_branch_set(ui, command, sub_args), - BranchCommand::Delete(sub_args) => cmd_branch_delete(ui, command, sub_args), - BranchCommand::Forget(sub_args) => cmd_branch_forget(ui, command, sub_args), - BranchCommand::Track(sub_args) => cmd_branch_track(ui, command, sub_args), - BranchCommand::Untrack(sub_args) => cmd_branch_untrack(ui, command, sub_args), - BranchCommand::List(sub_args) => cmd_branch_list(ui, command, sub_args), - } -} - -fn cmd_branch_create( - ui: &mut Ui, - command: &CommandHelper, - args: &BranchCreateArgs, -) -> Result<(), CommandError> { - let mut workspace_command = command.workspace_helper(ui)?; - let target_commit = - workspace_command.resolve_single_rev(args.revision.as_ref().unwrap_or(&RevisionArg::AT))?; - let view = workspace_command.repo().view(); - let branch_names = &args.names; - if let Some(branch_name) = branch_names - .iter() - .find(|&name| view.get_local_branch(name).is_present()) - { - return Err(user_error_with_hint( - format!("Branch already exists: {branch_name}"), - "Use `jj branch set` to update it.", - )); - } - - if branch_names.len() > 1 { - writeln!( - ui.warning_default(), - "Creating multiple branches: {}", - branch_names.join(", "), - )?; - } - - let mut tx = workspace_command.start_transaction(); - for branch_name in branch_names { - tx.mut_repo() - .set_local_branch_target(branch_name, RefTarget::normal(target_commit.id().clone())); - } - tx.finish( - ui, - format!( - "create {} pointing to commit {}", - make_branch_term(branch_names), - target_commit.id().hex() - ), - )?; - Ok(()) -} - -fn cmd_branch_rename( - ui: &mut Ui, - command: &CommandHelper, - args: &BranchRenameArgs, -) -> Result<(), CommandError> { - let mut workspace_command = command.workspace_helper(ui)?; - let view = workspace_command.repo().view(); - let old_branch = &args.old; - let ref_target = view.get_local_branch(old_branch).clone(); - if ref_target.is_absent() { - return Err(user_error(format!("No such branch: {old_branch}"))); - } - - let new_branch = &args.new; - if view.get_local_branch(new_branch).is_present() { - return Err(user_error(format!("Branch already exists: {new_branch}"))); - } - - let mut tx = workspace_command.start_transaction(); - tx.mut_repo() - .set_local_branch_target(new_branch, ref_target); - tx.mut_repo() - .set_local_branch_target(old_branch, RefTarget::absent()); - tx.finish( - ui, - format!( - "rename {} to {}", - make_branch_term(&[old_branch]), - make_branch_term(&[new_branch]), - ), - )?; - - let view = workspace_command.repo().view(); - if view - .remote_branches_matching( - &StringPattern::exact(old_branch), - &StringPattern::everything(), - ) - .any(|(_, remote_ref)| remote_ref.is_tracking()) - { - writeln!( - ui.warning_default(), - "Branch {old_branch} has tracking remote branches which were not renamed." - )?; - if let Some(mut writer) = ui.hint_default() { - writeln!( - writer, - "to rename the branch on the remote, you can `jj git push --branch {old_branch}` \ - first (to delete it on the remote), and then `jj git push --branch \ - {new_branch}`. `jj git push --all` would also be sufficient." - )?; - } - } - - Ok(()) -} - -fn cmd_branch_set( - ui: &mut Ui, - command: &CommandHelper, - args: &BranchSetArgs, -) -> Result<(), CommandError> { - let mut workspace_command = command.workspace_helper(ui)?; - let target_commit = - workspace_command.resolve_single_rev(args.revision.as_ref().unwrap_or(&RevisionArg::AT))?; - let repo = workspace_command.repo().as_ref(); - let is_fast_forward = |old_target: &RefTarget| { - // Strictly speaking, "all" old targets should be ancestors, but we allow - // conflict resolution by setting branch to "any" of the old target descendants. - old_target - .added_ids() - .any(|old| repo.index().is_ancestor(old, target_commit.id())) - }; - let branch_names = &args.names; - for name in branch_names { - let old_target = repo.view().get_local_branch(name); - if old_target.is_absent() { - return Err(user_error_with_hint( - format!("No such branch: {name}"), - "Use `jj branch create` to create it.", - )); - } - if !args.allow_backwards && !is_fast_forward(old_target) { - return Err(user_error_with_hint( - format!("Refusing to move branch backwards or sideways: {name}"), - "Use --allow-backwards to allow it.", - )); - } - } - - if branch_names.len() > 1 { - writeln!( - ui.warning_default(), - "Updating multiple branches: {}", - branch_names.join(", "), - )?; - } - - let mut tx = workspace_command.start_transaction(); - for branch_name in branch_names { - tx.mut_repo() - .set_local_branch_target(branch_name, RefTarget::normal(target_commit.id().clone())); - } - tx.finish( - ui, - format!( - "point {} to commit {}", - make_branch_term(branch_names), - target_commit.id().hex() - ), - )?; - Ok(()) -} - -fn find_local_branches( - view: &View, - name_patterns: &[StringPattern], -) -> Result, CommandError> { - find_branches_with(name_patterns, |pattern| { - view.local_branches_matching(pattern) - .map(|(name, _)| name.to_owned()) - }) -} - -fn find_forgettable_branches( - view: &View, - name_patterns: &[StringPattern], -) -> Result, CommandError> { - find_branches_with(name_patterns, |pattern| { - view.branches() - .filter(|(name, _)| pattern.matches(name)) - .map(|(name, _)| name.to_owned()) - }) -} - -fn find_branches_with<'a, I: Iterator>( - name_patterns: &'a [StringPattern], - mut find_matches: impl FnMut(&'a StringPattern) -> I, -) -> Result, CommandError> { - let mut matching_branches: Vec = vec![]; - let mut unmatched_patterns = vec![]; - for pattern in name_patterns { - let mut names = find_matches(pattern).peekable(); - if names.peek().is_none() { - unmatched_patterns.push(pattern); - } - matching_branches.extend(names); - } - match &unmatched_patterns[..] { - [] => { - matching_branches.sort_unstable(); - matching_branches.dedup(); - Ok(matching_branches) - } - [pattern] if pattern.is_exact() => Err(user_error(format!("No such branch: {pattern}"))), - patterns => Err(user_error(format!( - "No matching branches for patterns: {}", - patterns.iter().join(", ") - ))), - } -} - -fn find_remote_branches<'a>( - view: &'a View, - name_patterns: &[RemoteBranchNamePattern], -) -> Result, CommandError> { - let mut matching_branches = vec![]; - let mut unmatched_patterns = vec![]; - for pattern in name_patterns { - let mut matches = view - .remote_branches_matching(&pattern.branch, &pattern.remote) - .map(|((branch, remote), remote_ref)| { - let name = RemoteBranchName { - branch: branch.to_owned(), - remote: remote.to_owned(), - }; - (name, remote_ref) - }) - .peekable(); - if matches.peek().is_none() { - unmatched_patterns.push(pattern); - } - matching_branches.extend(matches); - } - match &unmatched_patterns[..] { - [] => { - matching_branches.sort_unstable_by(|(name1, _), (name2, _)| name1.cmp(name2)); - matching_branches.dedup_by(|(name1, _), (name2, _)| name1 == name2); - Ok(matching_branches) - } - [pattern] if pattern.is_exact() => { - Err(user_error(format!("No such remote branch: {pattern}"))) - } - patterns => Err(user_error(format!( - "No matching remote branches for patterns: {}", - patterns.iter().join(", ") - ))), - } -} - -fn cmd_branch_delete( - ui: &mut Ui, - command: &CommandHelper, - args: &BranchDeleteArgs, -) -> Result<(), CommandError> { - let mut workspace_command = command.workspace_helper(ui)?; - let view = workspace_command.repo().view(); - if !args.glob.is_empty() { - writeln!( - ui.warning_default(), - "--glob has been deprecated. Please prefix the pattern with `glob:` instead." - )?; - } - let name_patterns = [&args.names[..], &args.glob[..]].concat(); - let names = find_local_branches(view, &name_patterns)?; - let mut tx = workspace_command.start_transaction(); - for branch_name in names.iter() { - tx.mut_repo() - .set_local_branch_target(branch_name, RefTarget::absent()); - } - tx.finish(ui, format!("delete {}", make_branch_term(&names)))?; - if names.len() > 1 { - writeln!(ui.status(), "Deleted {} branches.", names.len())?; - } - Ok(()) -} - -fn cmd_branch_forget( - ui: &mut Ui, - command: &CommandHelper, - args: &BranchForgetArgs, -) -> Result<(), CommandError> { - let mut workspace_command = command.workspace_helper(ui)?; - let view = workspace_command.repo().view(); - if !args.glob.is_empty() { - writeln!( - ui.warning_default(), - "--glob has been deprecated. Please prefix the pattern with `glob:` instead." - )?; - } - let name_patterns = [&args.names[..], &args.glob[..]].concat(); - let names = find_forgettable_branches(view, &name_patterns)?; - let mut tx = workspace_command.start_transaction(); - for branch_name in names.iter() { - tx.mut_repo().remove_branch(branch_name); - } - tx.finish(ui, format!("forget {}", make_branch_term(&names)))?; - if names.len() > 1 { - writeln!(ui.status(), "Forgot {} branches.", names.len())?; - } - Ok(()) -} - -fn cmd_branch_track( - ui: &mut Ui, - command: &CommandHelper, - args: &BranchTrackArgs, -) -> Result<(), CommandError> { - let mut workspace_command = command.workspace_helper(ui)?; - let view = workspace_command.repo().view(); - let mut names = Vec::new(); - for (name, remote_ref) in find_remote_branches(view, &args.names)? { - if remote_ref.is_tracking() { - writeln!( - ui.warning_default(), - "Remote branch already tracked: {name}" - )?; - } else { - names.push(name); - } - } - let mut tx = workspace_command.start_transaction(); - for name in &names { - tx.mut_repo() - .track_remote_branch(&name.branch, &name.remote); - } - tx.finish(ui, format!("track remote {}", make_branch_term(&names)))?; - if names.len() > 1 { - writeln!( - ui.status(), - "Started tracking {} remote branches.", - names.len() - )?; - } - - //show conflicted branches if there are some - - if let Some(mut formatter) = ui.status_formatter() { - let template = { - let language = workspace_command.commit_template_language()?; - let text = command - .settings() - .config() - .get::("templates.branch_list")?; - workspace_command - .parse_template(&language, &text, CommitTemplateLanguage::wrap_ref_name)? - .labeled("branch_list") - }; - - let mut remote_per_branch: HashMap<&str, Vec<&str>> = HashMap::new(); - for n in names.iter() { - remote_per_branch - .entry(&n.branch) - .or_default() - .push(&n.remote); - } - let branches_to_list = - workspace_command - .repo() - .view() - .branches() - .filter(|(name, target)| { - remote_per_branch.contains_key(name) && target.local_target.has_conflict() - }); - - for (name, branch_target) in branches_to_list { - let local_target = branch_target.local_target; - let ref_name = RefName::local( - name, - local_target.clone(), - branch_target.remote_refs.iter().map(|x| x.1), - ); - template.format(&ref_name, formatter.as_mut())?; - - for (remote_name, remote_ref) in branch_target.remote_refs { - if remote_per_branch[name].contains(&remote_name) { - let ref_name = - RefName::remote(name, remote_name, remote_ref.clone(), local_target); - template.format(&ref_name, formatter.as_mut())?; - } - } - } - } - Ok(()) -} - -fn cmd_branch_untrack( - ui: &mut Ui, - command: &CommandHelper, - args: &BranchUntrackArgs, -) -> Result<(), CommandError> { - let mut workspace_command = command.workspace_helper(ui)?; - let view = workspace_command.repo().view(); - let mut names = Vec::new(); - for (name, remote_ref) in find_remote_branches(view, &args.names)? { - if name.remote == git::REMOTE_NAME_FOR_LOCAL_GIT_REPO { - // This restriction can be lifted if we want to support untracked @git branches. - writeln!( - ui.warning_default(), - "Git-tracking branch cannot be untracked: {name}" - )?; - } else if !remote_ref.is_tracking() { - writeln!( - ui.warning_default(), - "Remote branch not tracked yet: {name}" - )?; - } else { - names.push(name); - } - } - let mut tx = workspace_command.start_transaction(); - for name in &names { - tx.mut_repo() - .untrack_remote_branch(&name.branch, &name.remote); - } - tx.finish(ui, format!("untrack remote {}", make_branch_term(&names)))?; - if names.len() > 1 { - writeln!( - ui.status(), - "Stopped tracking {} remote branches.", - names.len() - )?; - } - Ok(()) -} - -fn cmd_branch_list( - ui: &mut Ui, - command: &CommandHelper, - args: &BranchListArgs, -) -> Result<(), CommandError> { - let workspace_command = command.workspace_helper(ui)?; - let repo = workspace_command.repo(); - let view = repo.view(); - - // Like cmd_git_push(), names and revisions are OR-ed. - let branch_names_to_list = if !args.names.is_empty() || !args.revisions.is_empty() { - let mut branch_names: HashSet<&str> = HashSet::new(); - if !args.names.is_empty() { - branch_names.extend( - view.branches() - .filter(|&(name, _)| args.names.iter().any(|pattern| pattern.matches(name))) - .map(|(name, _)| name), - ); - } - if !args.revisions.is_empty() { - // Match against local targets only, which is consistent with "jj git push". - let mut expression = workspace_command.parse_union_revsets(&args.revisions)?; - // Intersects with the set of local branch targets to minimize the lookup space. - expression.intersect_with(&RevsetExpression::branches(StringPattern::everything())); - let filtered_targets: HashSet<_> = expression.evaluate_to_commit_ids()?.collect(); - branch_names.extend( - view.local_branches() - .filter(|(_, target)| { - target.added_ids().any(|id| filtered_targets.contains(id)) - }) - .map(|(name, _)| name), - ); - } - Some(branch_names) - } else { - None - }; - - let template = { - let language = workspace_command.commit_template_language()?; - let text = match &args.template { - Some(value) => value.to_owned(), - None => command.settings().config().get("templates.branch_list")?, - }; - workspace_command - .parse_template(&language, &text, CommitTemplateLanguage::wrap_ref_name)? - .labeled("branch_list") - }; - - ui.request_pager(); - let mut formatter = ui.stdout_formatter(); - - let mut found_deleted_local_branch = false; - let mut found_deleted_tracking_local_branch = false; - let branches_to_list = view.branches().filter(|(name, target)| { - branch_names_to_list - .as_ref() - .map_or(true, |branch_names| branch_names.contains(name)) - && (!args.conflicted || target.local_target.has_conflict()) - }); - for (name, branch_target) in branches_to_list { - let local_target = branch_target.local_target; - let remote_refs = branch_target.remote_refs; - let (mut tracking_remote_refs, untracked_remote_refs) = remote_refs - .iter() - .copied() - .partition::, _>(|&(_, remote_ref)| remote_ref.is_tracking()); - - if args.tracked { - tracking_remote_refs - .retain(|&(remote, _)| remote != git::REMOTE_NAME_FOR_LOCAL_GIT_REPO); - } else if !args.all_remotes { - tracking_remote_refs.retain(|&(_, remote_ref)| remote_ref.target != *local_target); - } - - if !args.tracked && local_target.is_present() || !tracking_remote_refs.is_empty() { - let ref_name = RefName::local( - name, - local_target.clone(), - remote_refs.iter().map(|&(_, remote_ref)| remote_ref), - ); - template.format(&ref_name, formatter.as_mut())?; - } - - for &(remote, remote_ref) in &tracking_remote_refs { - let ref_name = RefName::remote(name, remote, remote_ref.clone(), local_target); - template.format(&ref_name, formatter.as_mut())?; - } - - if local_target.is_absent() && !tracking_remote_refs.is_empty() { - found_deleted_local_branch = true; - found_deleted_tracking_local_branch |= tracking_remote_refs - .iter() - .any(|&(remote, _)| remote != git::REMOTE_NAME_FOR_LOCAL_GIT_REPO); - } - - if args.all_remotes { - for &(remote, remote_ref) in &untracked_remote_refs { - let ref_name = RefName::remote_only(name, remote, remote_ref.target.clone()); - template.format(&ref_name, formatter.as_mut())?; - } - } - } - - drop(formatter); - - // Print only one of these hints. It's not important to mention unexported - // branches, but user might wonder why deleted branches are still listed. - if found_deleted_tracking_local_branch { - if let Some(mut writer) = ui.hint_default() { - writeln!( - writer, - "Branches marked as deleted will be *deleted permanently* on the remote on the \ - next `jj git push`. Use `jj branch forget` to prevent this." - )?; - } - } else if found_deleted_local_branch { - if let Some(mut writer) = ui.hint_default() { - writeln!( - writer, - "Branches marked as deleted will be deleted from the underlying Git repo on the \ - next `jj git export`." - )?; - } - } - - Ok(()) -} diff --git a/cli/src/commands/branch/create.rs b/cli/src/commands/branch/create.rs new file mode 100644 index 0000000000..6bdaaed3f9 --- /dev/null +++ b/cli/src/commands/branch/create.rs @@ -0,0 +1,86 @@ +// Copyright 2020-2023 The Jujutsu Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use clap::builder::NonEmptyStringValueParser; +use jj_lib::object_id::ObjectId as _; +use jj_lib::op_store::RefTarget; + +use super::has_tracked_remote_branches; +use crate::cli_util::{CommandHelper, RevisionArg}; +use crate::command_error::{user_error_with_hint, CommandError}; +use crate::ui::Ui; + +/// Create a new branch +#[derive(clap::Args, Clone, Debug)] +pub struct BranchCreateArgs { + /// The branch's target revision + #[arg(long, short)] + revision: Option, + + /// The branches to create + #[arg(required = true, value_parser = NonEmptyStringValueParser::new())] + names: Vec, +} + +pub fn cmd_branch_create( + ui: &mut Ui, + command: &CommandHelper, + args: &BranchCreateArgs, +) -> Result<(), CommandError> { + let mut workspace_command = command.workspace_helper(ui)?; + let target_commit = + workspace_command.resolve_single_rev(args.revision.as_ref().unwrap_or(&RevisionArg::AT))?; + let view = workspace_command.repo().view(); + let branch_names = &args.names; + for name in branch_names { + if view.get_local_branch(name).is_present() { + return Err(user_error_with_hint( + format!("Branch already exists: {name}"), + "Use `jj branch set` to update it.", + )); + } + if has_tracked_remote_branches(view, name) { + return Err(user_error_with_hint( + format!("Tracked remote branches exist for deleted branch: {name}"), + format!( + "Use `jj branch set` to recreate the local branch. Run `jj branch untrack \ + 'glob:{name}@*'` to disassociate them." + ), + )); + } + } + + if branch_names.len() > 1 { + writeln!( + ui.warning_default(), + "Creating multiple branches: {}", + branch_names.join(", "), + )?; + } + + let mut tx = workspace_command.start_transaction(); + for branch_name in branch_names { + tx.mut_repo() + .set_local_branch_target(branch_name, RefTarget::normal(target_commit.id().clone())); + } + tx.finish( + ui, + format!( + "create branch {names} pointing to commit {id}", + names = branch_names.join(", "), + id = target_commit.id().hex() + ), + )?; + Ok(()) +} diff --git a/cli/src/commands/branch/delete.rs b/cli/src/commands/branch/delete.rs new file mode 100644 index 0000000000..c9d5700766 --- /dev/null +++ b/cli/src/commands/branch/delete.rs @@ -0,0 +1,61 @@ +// Copyright 2020-2023 The Jujutsu Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use itertools::Itertools as _; +use jj_lib::op_store::RefTarget; +use jj_lib::str_util::StringPattern; + +use super::find_local_branches; +use crate::cli_util::CommandHelper; +use crate::command_error::CommandError; +use crate::ui::Ui; + +/// Delete an existing branch and propagate the deletion to remotes on the +/// next push +#[derive(clap::Args, Clone, Debug)] +pub struct BranchDeleteArgs { + /// The branches to delete + /// + /// By default, the specified name matches exactly. Use `glob:` prefix to + /// select branches by wildcard pattern. For details, see + /// https://github.com/martinvonz/jj/blob/main/docs/revsets.md#string-patterns. + #[arg(required = true, value_parser = StringPattern::parse)] + names: Vec, +} + +pub fn cmd_branch_delete( + ui: &mut Ui, + command: &CommandHelper, + args: &BranchDeleteArgs, +) -> Result<(), CommandError> { + let mut workspace_command = command.workspace_helper(ui)?; + let repo = workspace_command.repo().clone(); + let matched_branches = find_local_branches(repo.view(), &args.names)?; + let mut tx = workspace_command.start_transaction(); + for (name, _) in &matched_branches { + tx.mut_repo() + .set_local_branch_target(name, RefTarget::absent()); + } + tx.finish( + ui, + format!( + "delete branch {}", + matched_branches.iter().map(|(name, _)| name).join(", ") + ), + )?; + if matched_branches.len() > 1 { + writeln!(ui.status(), "Deleted {} branches.", matched_branches.len())?; + } + Ok(()) +} diff --git a/cli/src/commands/branch/forget.rs b/cli/src/commands/branch/forget.rs new file mode 100644 index 0000000000..a0b3b05a56 --- /dev/null +++ b/cli/src/commands/branch/forget.rs @@ -0,0 +1,78 @@ +// Copyright 2020-2023 The Jujutsu Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use itertools::Itertools as _; +use jj_lib::op_store::{BranchTarget, RefTarget, RemoteRef}; +use jj_lib::str_util::StringPattern; +use jj_lib::view::View; + +use super::find_branches_with; +use crate::cli_util::CommandHelper; +use crate::command_error::CommandError; +use crate::ui::Ui; + +/// Forget everything about a branch, including its local and remote +/// targets +/// +/// A forgotten branch will not impact remotes on future pushes. It will be +/// recreated on future pulls if it still exists in the remote. +#[derive(clap::Args, Clone, Debug)] +pub struct BranchForgetArgs { + /// The branches to forget + /// + /// By default, the specified name matches exactly. Use `glob:` prefix to + /// select branches by wildcard pattern. For details, see + /// https://github.com/martinvonz/jj/blob/main/docs/revsets.md#string-patterns. + #[arg(required = true, value_parser = StringPattern::parse)] + names: Vec, +} + +pub fn cmd_branch_forget( + ui: &mut Ui, + command: &CommandHelper, + args: &BranchForgetArgs, +) -> Result<(), CommandError> { + let mut workspace_command = command.workspace_helper(ui)?; + let repo = workspace_command.repo().clone(); + let matched_branches = find_forgettable_branches(repo.view(), &args.names)?; + let mut tx = workspace_command.start_transaction(); + for (name, branch_target) in &matched_branches { + tx.mut_repo() + .set_local_branch_target(name, RefTarget::absent()); + for (remote_name, _) in &branch_target.remote_refs { + tx.mut_repo() + .set_remote_branch(name, remote_name, RemoteRef::absent()); + } + } + tx.finish( + ui, + format!( + "forget branch {}", + matched_branches.iter().map(|(name, _)| name).join(", ") + ), + )?; + if matched_branches.len() > 1 { + writeln!(ui.status(), "Forgot {} branches.", matched_branches.len())?; + } + Ok(()) +} + +fn find_forgettable_branches<'a>( + view: &'a View, + name_patterns: &[StringPattern], +) -> Result)>, CommandError> { + find_branches_with(name_patterns, |pattern| { + view.branches().filter(|(name, _)| pattern.matches(name)) + }) +} diff --git a/cli/src/commands/branch/list.rs b/cli/src/commands/branch/list.rs new file mode 100644 index 0000000000..150875cf14 --- /dev/null +++ b/cli/src/commands/branch/list.rs @@ -0,0 +1,199 @@ +// Copyright 2020-2023 The Jujutsu Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::collections::HashSet; + +use jj_lib::git; +use jj_lib::revset::RevsetExpression; +use jj_lib::str_util::StringPattern; + +use crate::cli_util::{CommandHelper, RevisionArg}; +use crate::command_error::CommandError; +use crate::commit_templater::{CommitTemplateLanguage, RefName}; +use crate::ui::Ui; + +/// List branches and their targets +/// +/// By default, a tracking remote branch will be included only if its target is +/// different from the local target. A non-tracking remote branch won't be +/// listed. For a conflicted branch (both local and remote), old target +/// revisions are preceded by a "-" and new target revisions are preceded by a +/// "+". +/// +/// For information about branches, see +/// https://github.com/martinvonz/jj/blob/main/docs/branches.md. +#[derive(clap::Args, Clone, Debug)] +pub struct BranchListArgs { + /// Show all tracking and non-tracking remote branches including the ones + /// whose targets are synchronized with the local branches + #[arg(long, short, alias = "all")] + all_remotes: bool, + + /// Show remote tracked branches only. Omits local Git-tracking branches by + /// default + #[arg(long, short, conflicts_with_all = ["all_remotes"])] + tracked: bool, + + /// Show conflicted branches only + #[arg(long, short, conflicts_with_all = ["all_remotes"])] + conflicted: bool, + + /// Show branches whose local name matches + /// + /// By default, the specified name matches exactly. Use `glob:` prefix to + /// select branches by wildcard pattern. For details, see + /// https://github.com/martinvonz/jj/blob/main/docs/revsets.md#string-patterns. + #[arg(value_parser = StringPattern::parse)] + names: Vec, + + /// Show branches whose local targets are in the given revisions + /// + /// Note that `-r deleted_branch` will not work since `deleted_branch` + /// wouldn't have a local target. + #[arg(long, short)] + revisions: Vec, + + /// Render each branch using the given template + /// + /// All 0-argument methods of the `RefName` type are available as keywords. + /// + /// For the syntax, see https://github.com/martinvonz/jj/blob/main/docs/templates.md + #[arg(long, short = 'T')] + template: Option, +} + +pub fn cmd_branch_list( + ui: &mut Ui, + command: &CommandHelper, + args: &BranchListArgs, +) -> Result<(), CommandError> { + let workspace_command = command.workspace_helper(ui)?; + let repo = workspace_command.repo(); + let view = repo.view(); + + // Like cmd_git_push(), names and revisions are OR-ed. + let branch_names_to_list = if !args.names.is_empty() || !args.revisions.is_empty() { + let mut branch_names: HashSet<&str> = HashSet::new(); + if !args.names.is_empty() { + branch_names.extend( + view.branches() + .filter(|&(name, _)| args.names.iter().any(|pattern| pattern.matches(name))) + .map(|(name, _)| name), + ); + } + if !args.revisions.is_empty() { + // Match against local targets only, which is consistent with "jj git push". + let mut expression = workspace_command.parse_union_revsets(&args.revisions)?; + // Intersects with the set of local branch targets to minimize the lookup space. + expression.intersect_with(&RevsetExpression::branches(StringPattern::everything())); + let filtered_targets: HashSet<_> = expression.evaluate_to_commit_ids()?.collect(); + branch_names.extend( + view.local_branches() + .filter(|(_, target)| { + target.added_ids().any(|id| filtered_targets.contains(id)) + }) + .map(|(name, _)| name), + ); + } + Some(branch_names) + } else { + None + }; + + let template = { + let language = workspace_command.commit_template_language()?; + let text = match &args.template { + Some(value) => value.to_owned(), + None => command.settings().config().get("templates.branch_list")?, + }; + workspace_command + .parse_template(&language, &text, CommitTemplateLanguage::wrap_ref_name)? + .labeled("branch_list") + }; + + ui.request_pager(); + let mut formatter = ui.stdout_formatter(); + + let mut found_deleted_local_branch = false; + let mut found_deleted_tracking_local_branch = false; + let branches_to_list = view.branches().filter(|(name, target)| { + branch_names_to_list + .as_ref() + .map_or(true, |branch_names| branch_names.contains(name)) + && (!args.conflicted || target.local_target.has_conflict()) + }); + for (name, branch_target) in branches_to_list { + let local_target = branch_target.local_target; + let remote_refs = branch_target.remote_refs; + let (mut tracking_remote_refs, untracked_remote_refs) = remote_refs + .iter() + .copied() + .partition::, _>(|&(_, remote_ref)| remote_ref.is_tracking()); + + if args.tracked { + tracking_remote_refs + .retain(|&(remote, _)| remote != git::REMOTE_NAME_FOR_LOCAL_GIT_REPO); + } else if !args.all_remotes { + tracking_remote_refs.retain(|&(_, remote_ref)| remote_ref.target != *local_target); + } + + if !args.tracked && local_target.is_present() || !tracking_remote_refs.is_empty() { + let ref_name = RefName::local( + name, + local_target.clone(), + remote_refs.iter().map(|&(_, remote_ref)| remote_ref), + ); + template.format(&ref_name, formatter.as_mut())?; + } + + for &(remote, remote_ref) in &tracking_remote_refs { + let ref_name = RefName::remote(name, remote, remote_ref.clone(), local_target); + template.format(&ref_name, formatter.as_mut())?; + } + + if local_target.is_absent() && !tracking_remote_refs.is_empty() { + found_deleted_local_branch = true; + found_deleted_tracking_local_branch |= tracking_remote_refs + .iter() + .any(|&(remote, _)| remote != git::REMOTE_NAME_FOR_LOCAL_GIT_REPO); + } + + if args.all_remotes { + for &(remote, remote_ref) in &untracked_remote_refs { + let ref_name = RefName::remote_only(name, remote, remote_ref.target.clone()); + template.format(&ref_name, formatter.as_mut())?; + } + } + } + + drop(formatter); + + // Print only one of these hints. It's not important to mention unexported + // branches, but user might wonder why deleted branches are still listed. + if found_deleted_tracking_local_branch { + writeln!( + ui.hint_default(), + "Branches marked as deleted will be *deleted permanently* on the remote on the next \ + `jj git push`. Use `jj branch forget` to prevent this." + )?; + } else if found_deleted_local_branch { + writeln!( + ui.hint_default(), + "Branches marked as deleted will be deleted from the underlying Git repo on the next \ + `jj git export`." + )?; + } + + Ok(()) +} diff --git a/cli/src/commands/branch/mod.rs b/cli/src/commands/branch/mod.rs new file mode 100644 index 0000000000..70452c5f21 --- /dev/null +++ b/cli/src/commands/branch/mod.rs @@ -0,0 +1,179 @@ +// Copyright 2020-2023 The Jujutsu Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +mod create; +mod delete; +mod forget; +mod list; +mod r#move; +mod rename; +mod set; +mod track; +mod untrack; + +use itertools::Itertools as _; +use jj_lib::backend::CommitId; +use jj_lib::op_store::{RefTarget, RemoteRef}; +use jj_lib::repo::Repo; +use jj_lib::str_util::StringPattern; +use jj_lib::view::View; + +use self::create::{cmd_branch_create, BranchCreateArgs}; +use self::delete::{cmd_branch_delete, BranchDeleteArgs}; +use self::forget::{cmd_branch_forget, BranchForgetArgs}; +use self::list::{cmd_branch_list, BranchListArgs}; +use self::r#move::{cmd_branch_move, BranchMoveArgs}; +use self::rename::{cmd_branch_rename, BranchRenameArgs}; +use self::set::{cmd_branch_set, BranchSetArgs}; +use self::track::{cmd_branch_track, BranchTrackArgs}; +use self::untrack::{cmd_branch_untrack, BranchUntrackArgs}; +use crate::cli_util::{CommandHelper, RemoteBranchName, RemoteBranchNamePattern}; +use crate::command_error::{user_error, CommandError}; +use crate::ui::Ui; + +/// Manage branches +/// +/// For information about branches, see +/// https://github.com/martinvonz/jj/blob/main/docs/branches.md. +#[derive(clap::Subcommand, Clone, Debug)] +pub enum BranchCommand { + #[command(visible_alias("c"))] + Create(BranchCreateArgs), + #[command(visible_alias("d"))] + Delete(BranchDeleteArgs), + #[command(visible_alias("f"))] + Forget(BranchForgetArgs), + #[command(visible_alias("l"))] + List(BranchListArgs), + #[command(visible_alias("m"))] + Move(BranchMoveArgs), + #[command(visible_alias("r"))] + Rename(BranchRenameArgs), + #[command(visible_alias("s"))] + Set(BranchSetArgs), + #[command(visible_alias("t"))] + Track(BranchTrackArgs), + Untrack(BranchUntrackArgs), +} + +pub fn cmd_branch( + ui: &mut Ui, + command: &CommandHelper, + subcommand: &BranchCommand, +) -> Result<(), CommandError> { + match subcommand { + BranchCommand::Create(args) => cmd_branch_create(ui, command, args), + BranchCommand::Delete(args) => cmd_branch_delete(ui, command, args), + BranchCommand::Forget(args) => cmd_branch_forget(ui, command, args), + BranchCommand::List(args) => cmd_branch_list(ui, command, args), + BranchCommand::Move(args) => cmd_branch_move(ui, command, args), + BranchCommand::Rename(args) => cmd_branch_rename(ui, command, args), + BranchCommand::Set(args) => cmd_branch_set(ui, command, args), + BranchCommand::Track(args) => cmd_branch_track(ui, command, args), + BranchCommand::Untrack(args) => cmd_branch_untrack(ui, command, args), + } +} + +fn find_local_branches<'a>( + view: &'a View, + name_patterns: &[StringPattern], +) -> Result, CommandError> { + find_branches_with(name_patterns, |pattern| { + view.local_branches_matching(pattern) + }) +} + +fn find_branches_with<'a, 'b, V, I: Iterator>( + name_patterns: &'b [StringPattern], + mut find_matches: impl FnMut(&'b StringPattern) -> I, +) -> Result, CommandError> { + let mut matching_branches: Vec = vec![]; + let mut unmatched_patterns = vec![]; + for pattern in name_patterns { + let mut matches = find_matches(pattern).peekable(); + if matches.peek().is_none() { + unmatched_patterns.push(pattern); + } + matching_branches.extend(matches); + } + match &unmatched_patterns[..] { + [] => { + matching_branches.sort_unstable_by_key(|(name, _)| *name); + matching_branches.dedup_by_key(|(name, _)| *name); + Ok(matching_branches) + } + [pattern] if pattern.is_exact() => Err(user_error(format!("No such branch: {pattern}"))), + patterns => Err(user_error(format!( + "No matching branches for patterns: {}", + patterns.iter().join(", ") + ))), + } +} + +fn find_remote_branches<'a>( + view: &'a View, + name_patterns: &[RemoteBranchNamePattern], +) -> Result, CommandError> { + let mut matching_branches = vec![]; + let mut unmatched_patterns = vec![]; + for pattern in name_patterns { + let mut matches = view + .remote_branches_matching(&pattern.branch, &pattern.remote) + .map(|((branch, remote), remote_ref)| { + let name = RemoteBranchName { + branch: branch.to_owned(), + remote: remote.to_owned(), + }; + (name, remote_ref) + }) + .peekable(); + if matches.peek().is_none() { + unmatched_patterns.push(pattern); + } + matching_branches.extend(matches); + } + match &unmatched_patterns[..] { + [] => { + matching_branches.sort_unstable_by(|(name1, _), (name2, _)| name1.cmp(name2)); + matching_branches.dedup_by(|(name1, _), (name2, _)| name1 == name2); + Ok(matching_branches) + } + [pattern] if pattern.is_exact() => { + Err(user_error(format!("No such remote branch: {pattern}"))) + } + patterns => Err(user_error(format!( + "No matching remote branches for patterns: {}", + patterns.iter().join(", ") + ))), + } +} + +/// Whether or not the `branch` has any tracked remotes (i.e. is a tracking +/// local branch.) +fn has_tracked_remote_branches(view: &View, branch: &str) -> bool { + view.remote_branches_matching(&StringPattern::exact(branch), &StringPattern::everything()) + .any(|(_, remote_ref)| remote_ref.is_tracking()) +} + +fn is_fast_forward(repo: &dyn Repo, old_target: &RefTarget, new_target_id: &CommitId) -> bool { + if old_target.is_present() { + // Strictly speaking, "all" old targets should be ancestors, but we allow + // conflict resolution by setting branch to "any" of the old target descendants. + old_target + .added_ids() + .any(|old| repo.index().is_ancestor(old, new_target_id)) + } else { + true + } +} diff --git a/cli/src/commands/branch/move.rs b/cli/src/commands/branch/move.rs new file mode 100644 index 0000000000..ac5b414212 --- /dev/null +++ b/cli/src/commands/branch/move.rs @@ -0,0 +1,135 @@ +// Copyright 2020-2023 The Jujutsu Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use itertools::Itertools as _; +use jj_lib::backend::CommitId; +use jj_lib::object_id::ObjectId as _; +use jj_lib::op_store::RefTarget; +use jj_lib::str_util::StringPattern; + +use super::{find_branches_with, is_fast_forward}; +use crate::cli_util::{CommandHelper, RevisionArg}; +use crate::command_error::{user_error_with_hint, CommandError}; +use crate::ui::Ui; + +/// Move existing branches to target revision +/// +/// If branch names are given, the specified branches will be updated to point +/// to the target revision. +/// +/// If `--from` options are given, branches currently pointing to the specified +/// revisions will be updated. The branches can also be filtered by names. +/// +/// Example: pull up the nearest branches to the working-copy parent +/// +/// $ jj branch move --from 'heads(::@- & branches())' --to @- +#[derive(clap::Args, Clone, Debug)] +#[command(group(clap::ArgGroup::new("source").multiple(true).required(true)))] +pub struct BranchMoveArgs { + /// Move branches from the given revisions + #[arg(long, group = "source", value_name = "REVISIONS")] + from: Vec, + + /// Move branches to this revision + #[arg(long, default_value = "@", value_name = "REVISION")] + to: RevisionArg, + + /// Allow moving branches backwards or sideways + #[arg(long, short = 'B')] + allow_backwards: bool, + + /// Move branches matching the given name patterns + /// + /// By default, the specified name matches exactly. Use `glob:` prefix to + /// select branches by wildcard pattern. For details, see + /// https://github.com/martinvonz/jj/blob/main/docs/revsets.md#string-patterns. + #[arg(group = "source", value_parser = StringPattern::parse)] + names: Vec, +} + +pub fn cmd_branch_move( + ui: &mut Ui, + command: &CommandHelper, + args: &BranchMoveArgs, +) -> Result<(), CommandError> { + let mut workspace_command = command.workspace_helper(ui)?; + let repo = workspace_command.repo().clone(); + + let matched_branches = { + let is_source_commit = if !args.from.is_empty() { + workspace_command + .parse_union_revsets(&args.from)? + .evaluate()? + .containing_fn() + } else { + Box::new(|_: &CommitId| true) + }; + if !args.names.is_empty() { + find_branches_with(&args.names, |pattern| { + repo.view() + .local_branches_matching(pattern) + .filter(|(_, target)| target.added_ids().any(&is_source_commit)) + })? + } else { + repo.view() + .local_branches() + .filter(|(_, target)| target.added_ids().any(&is_source_commit)) + .collect() + } + }; + let target_commit = workspace_command.resolve_single_rev(&args.to)?; + + if matched_branches.is_empty() { + writeln!(ui.status(), "No branches to update.")?; + return Ok(()); + } + if matched_branches.len() > 1 { + writeln!( + ui.warning_default(), + "Updating multiple branches: {}", + matched_branches.iter().map(|(name, _)| name).join(", "), + )?; + if args.names.is_empty() { + writeln!(ui.hint_default(), "Specify branch by name to update one.")?; + } + } + + if !args.allow_backwards { + if let Some((name, _)) = matched_branches + .iter() + .find(|(_, old_target)| !is_fast_forward(repo.as_ref(), old_target, target_commit.id())) + { + return Err(user_error_with_hint( + format!("Refusing to move branch backwards or sideways: {name}"), + "Use --allow-backwards to allow it.", + )); + } + } + + let mut tx = workspace_command.start_transaction(); + for (name, _) in &matched_branches { + tx.mut_repo() + .set_local_branch_target(name, RefTarget::normal(target_commit.id().clone())); + } + tx.finish( + ui, + format!( + "point branch {names} to commit {id}", + names = matched_branches.iter().map(|(name, _)| name).join(", "), + id = target_commit.id().hex() + ), + )?; + + Ok(()) +} diff --git a/cli/src/commands/branch/rename.rs b/cli/src/commands/branch/rename.rs new file mode 100644 index 0000000000..4d2ae459e2 --- /dev/null +++ b/cli/src/commands/branch/rename.rs @@ -0,0 +1,87 @@ +// Copyright 2020-2023 The Jujutsu Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use jj_lib::op_store::RefTarget; + +use super::has_tracked_remote_branches; +use crate::cli_util::CommandHelper; +use crate::command_error::{user_error, CommandError}; +use crate::ui::Ui; + +/// Rename `old` branch name to `new` branch name +/// +/// The new branch name points at the same commit as the old branch name. +#[derive(clap::Args, Clone, Debug)] +pub struct BranchRenameArgs { + /// The old name of the branch + old: String, + + /// The new name of the branch + new: String, +} + +pub fn cmd_branch_rename( + ui: &mut Ui, + command: &CommandHelper, + args: &BranchRenameArgs, +) -> Result<(), CommandError> { + let mut workspace_command = command.workspace_helper(ui)?; + let view = workspace_command.repo().view(); + let old_branch = &args.old; + let ref_target = view.get_local_branch(old_branch).clone(); + if ref_target.is_absent() { + return Err(user_error(format!("No such branch: {old_branch}"))); + } + + let new_branch = &args.new; + if view.get_local_branch(new_branch).is_present() { + return Err(user_error(format!("Branch already exists: {new_branch}"))); + } + + let mut tx = workspace_command.start_transaction(); + tx.mut_repo() + .set_local_branch_target(new_branch, ref_target); + tx.mut_repo() + .set_local_branch_target(old_branch, RefTarget::absent()); + tx.finish(ui, format!("rename branch {old_branch} to {new_branch}"))?; + + let view = workspace_command.repo().view(); + if has_tracked_remote_branches(view, old_branch) { + writeln!( + ui.warning_default(), + "Tracked remote branches for branch {old_branch} were not renamed.", + )?; + writeln!( + ui.hint_default(), + "To rename the branch on the remote, you can `jj git push --branch {old_branch}` \ + first (to delete it on the remote), and then `jj git push --branch {new_branch}`. \ + `jj git push --all` would also be sufficient." + )?; + } + if has_tracked_remote_branches(view, new_branch) { + // This isn't an error because branch renaming can't be propagated to + // the remote immediately. "rename old new && rename new old" should be + // allowed even if the original old branch had tracked remotes. + writeln!( + ui.warning_default(), + "Tracked remote branches for branch {new_branch} exist." + )?; + writeln!( + ui.hint_default(), + "Run `jj branch untrack 'glob:{new_branch}@*'` to disassociate them." + )?; + } + + Ok(()) +} diff --git a/cli/src/commands/branch/set.rs b/cli/src/commands/branch/set.rs new file mode 100644 index 0000000000..267e98b592 --- /dev/null +++ b/cli/src/commands/branch/set.rs @@ -0,0 +1,101 @@ +// Copyright 2020-2023 The Jujutsu Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use clap::builder::NonEmptyStringValueParser; +use jj_lib::object_id::ObjectId as _; +use jj_lib::op_store::RefTarget; + +use super::{has_tracked_remote_branches, is_fast_forward}; +use crate::cli_util::{CommandHelper, RevisionArg}; +use crate::command_error::{user_error_with_hint, CommandError}; +use crate::ui::Ui; + +/// Create or update a branch to point to a certain commit +#[derive(clap::Args, Clone, Debug)] +pub struct BranchSetArgs { + /// The branch's target revision + #[arg(long, short)] + revision: Option, + + /// Allow moving the branch backwards or sideways + #[arg(long, short = 'B')] + allow_backwards: bool, + + /// The branches to update + #[arg(required = true, value_parser = NonEmptyStringValueParser::new())] + names: Vec, +} + +pub fn cmd_branch_set( + ui: &mut Ui, + command: &CommandHelper, + args: &BranchSetArgs, +) -> Result<(), CommandError> { + let mut workspace_command = command.workspace_helper(ui)?; + let target_commit = + workspace_command.resolve_single_rev(args.revision.as_ref().unwrap_or(&RevisionArg::AT))?; + let repo = workspace_command.repo().as_ref(); + let branch_names = &args.names; + let mut new_branch_names: Vec<&str> = Vec::new(); + for name in branch_names { + let old_target = repo.view().get_local_branch(name); + // If a branch is absent locally but is still tracking remote branches, + // we are resurrecting the local branch, not "creating" a new branch. + if old_target.is_absent() && !has_tracked_remote_branches(repo.view(), name) { + new_branch_names.push(name); + } + if !args.allow_backwards && !is_fast_forward(repo, old_target, target_commit.id()) { + return Err(user_error_with_hint( + format!("Refusing to move branch backwards or sideways: {name}"), + "Use --allow-backwards to allow it.", + )); + } + } + + if branch_names.len() > 1 { + writeln!( + ui.warning_default(), + "Updating multiple branches: {}", + branch_names.join(", "), + )?; + } + + let mut tx = workspace_command.start_transaction(); + for branch_name in branch_names { + tx.mut_repo() + .set_local_branch_target(branch_name, RefTarget::normal(target_commit.id().clone())); + } + tx.finish( + ui, + format!( + "point branch {names} to commit {id}", + names = branch_names.join(", "), + id = target_commit.id().hex() + ), + )?; + + if !new_branch_names.is_empty() { + writeln!( + ui.status(), + "Created branches: {}", + new_branch_names.join(", "), + )?; + // TODO: delete this hint in jj 0.25+ + writeln!( + ui.hint_default(), + "Consider using `jj branch move` if your intention was to move existing branches." + )?; + } + Ok(()) +} diff --git a/cli/src/commands/branch/track.rs b/cli/src/commands/branch/track.rs new file mode 100644 index 0000000000..5dab3a29d0 --- /dev/null +++ b/cli/src/commands/branch/track.rs @@ -0,0 +1,127 @@ +// Copyright 2020-2023 The Jujutsu Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::collections::HashMap; + +use itertools::Itertools as _; + +use super::find_remote_branches; +use crate::cli_util::{CommandHelper, RemoteBranchNamePattern}; +use crate::command_error::CommandError; +use crate::commit_templater::{CommitTemplateLanguage, RefName}; +use crate::ui::Ui; + +/// Start tracking given remote branches +/// +/// A tracking remote branch will be imported as a local branch of the same +/// name. Changes to it will propagate to the existing local branch on future +/// pulls. +#[derive(clap::Args, Clone, Debug)] +pub struct BranchTrackArgs { + /// Remote branches to track + /// + /// By default, the specified name matches exactly. Use `glob:` prefix to + /// select branches by wildcard pattern. For details, see + /// https://github.com/martinvonz/jj/blob/main/docs/revsets.md#string-patterns. + /// + /// Examples: branch@remote, glob:main@*, glob:jjfan-*@upstream + #[arg(required = true, value_name = "BRANCH@REMOTE")] + names: Vec, +} + +pub fn cmd_branch_track( + ui: &mut Ui, + command: &CommandHelper, + args: &BranchTrackArgs, +) -> Result<(), CommandError> { + let mut workspace_command = command.workspace_helper(ui)?; + let view = workspace_command.repo().view(); + let mut names = Vec::new(); + for (name, remote_ref) in find_remote_branches(view, &args.names)? { + if remote_ref.is_tracking() { + writeln!( + ui.warning_default(), + "Remote branch already tracked: {name}" + )?; + } else { + names.push(name); + } + } + let mut tx = workspace_command.start_transaction(); + for name in &names { + tx.mut_repo() + .track_remote_branch(&name.branch, &name.remote); + } + tx.finish( + ui, + format!("track remote branch {}", names.iter().join(", ")), + )?; + if names.len() > 1 { + writeln!( + ui.status(), + "Started tracking {} remote branches.", + names.len() + )?; + } + + //show conflicted branches if there are some + + if let Some(mut formatter) = ui.status_formatter() { + let template = { + let language = workspace_command.commit_template_language()?; + let text = command + .settings() + .config() + .get::("templates.branch_list")?; + workspace_command + .parse_template(&language, &text, CommitTemplateLanguage::wrap_ref_name)? + .labeled("branch_list") + }; + + let mut remote_per_branch: HashMap<&str, Vec<&str>> = HashMap::new(); + for n in names.iter() { + remote_per_branch + .entry(&n.branch) + .or_default() + .push(&n.remote); + } + let branches_to_list = + workspace_command + .repo() + .view() + .branches() + .filter(|(name, target)| { + remote_per_branch.contains_key(name) && target.local_target.has_conflict() + }); + + for (name, branch_target) in branches_to_list { + let local_target = branch_target.local_target; + let ref_name = RefName::local( + name, + local_target.clone(), + branch_target.remote_refs.iter().map(|x| x.1), + ); + template.format(&ref_name, formatter.as_mut())?; + + for (remote_name, remote_ref) in branch_target.remote_refs { + if remote_per_branch[name].contains(&remote_name) { + let ref_name = + RefName::remote(name, remote_name, remote_ref.clone(), local_target); + template.format(&ref_name, formatter.as_mut())?; + } + } + } + } + Ok(()) +} diff --git a/cli/src/commands/branch/untrack.rs b/cli/src/commands/branch/untrack.rs new file mode 100644 index 0000000000..498ecfe29b --- /dev/null +++ b/cli/src/commands/branch/untrack.rs @@ -0,0 +1,81 @@ +// Copyright 2020-2023 The Jujutsu Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use itertools::Itertools as _; +use jj_lib::git; + +use super::find_remote_branches; +use crate::cli_util::{CommandHelper, RemoteBranchNamePattern}; +use crate::command_error::CommandError; +use crate::ui::Ui; + +/// Stop tracking given remote branches +/// +/// A non-tracking remote branch is just a pointer to the last-fetched remote +/// branch. It won't be imported as a local branch on future pulls. +#[derive(clap::Args, Clone, Debug)] +pub struct BranchUntrackArgs { + /// Remote branches to untrack + /// + /// By default, the specified name matches exactly. Use `glob:` prefix to + /// select branches by wildcard pattern. For details, see + /// https://github.com/martinvonz/jj/blob/main/docs/revsets.md#string-patterns. + /// + /// Examples: branch@remote, glob:main@*, glob:jjfan-*@upstream + #[arg(required = true, value_name = "BRANCH@REMOTE")] + names: Vec, +} + +pub fn cmd_branch_untrack( + ui: &mut Ui, + command: &CommandHelper, + args: &BranchUntrackArgs, +) -> Result<(), CommandError> { + let mut workspace_command = command.workspace_helper(ui)?; + let view = workspace_command.repo().view(); + let mut names = Vec::new(); + for (name, remote_ref) in find_remote_branches(view, &args.names)? { + if name.remote == git::REMOTE_NAME_FOR_LOCAL_GIT_REPO { + // This restriction can be lifted if we want to support untracked @git branches. + writeln!( + ui.warning_default(), + "Git-tracking branch cannot be untracked: {name}" + )?; + } else if !remote_ref.is_tracking() { + writeln!( + ui.warning_default(), + "Remote branch not tracked yet: {name}" + )?; + } else { + names.push(name); + } + } + let mut tx = workspace_command.start_transaction(); + for name in &names { + tx.mut_repo() + .untrack_remote_branch(&name.branch, &name.remote); + } + tx.finish( + ui, + format!("untrack remote branch {}", names.iter().join(", ")), + )?; + if names.len() > 1 { + writeln!( + ui.status(), + "Stopped tracking {} remote branches.", + names.len() + )?; + } + Ok(()) +} diff --git a/cli/src/commands/config.rs b/cli/src/commands/config.rs index 46e13910a1..4c33a0021d 100644 --- a/cli/src/commands/config.rs +++ b/cli/src/commands/config.rs @@ -63,16 +63,16 @@ impl ConfigLevelArgs { /// config, see https://github.com/martinvonz/jj/blob/main/docs/config.md. #[derive(clap::Subcommand, Clone, Debug)] pub(crate) enum ConfigCommand { - #[command(visible_alias("l"))] - List(ConfigListArgs), - #[command(visible_alias("g"))] - Get(ConfigGetArgs), - #[command(visible_alias("s"))] - Set(ConfigSetArgs), #[command(visible_alias("e"))] Edit(ConfigEditArgs), + #[command(visible_alias("g"))] + Get(ConfigGetArgs), + #[command(visible_alias("l"))] + List(ConfigListArgs), #[command(visible_alias("p"))] Path(ConfigPathArgs), + #[command(visible_alias("s"))] + Set(ConfigSetArgs), } /// List variables set in config file, along with their values. @@ -158,11 +158,11 @@ pub(crate) fn cmd_config( subcommand: &ConfigCommand, ) -> Result<(), CommandError> { match subcommand { - ConfigCommand::List(sub_args) => cmd_config_list(ui, command, sub_args), - ConfigCommand::Get(sub_args) => cmd_config_get(ui, command, sub_args), - ConfigCommand::Set(sub_args) => cmd_config_set(ui, command, sub_args), - ConfigCommand::Edit(sub_args) => cmd_config_edit(ui, command, sub_args), - ConfigCommand::Path(sub_args) => cmd_config_path(ui, command, sub_args), + ConfigCommand::Edit(args) => cmd_config_edit(ui, command, args), + ConfigCommand::Get(args) => cmd_config_get(ui, command, args), + ConfigCommand::List(args) => cmd_config_list(ui, command, args), + ConfigCommand::Path(args) => cmd_config_path(ui, command, args), + ConfigCommand::Set(args) => cmd_config_set(ui, command, args), } } diff --git a/cli/src/commands/debug.rs b/cli/src/commands/debug.rs deleted file mode 100644 index 79401c0cb1..0000000000 --- a/cli/src/commands/debug.rs +++ /dev/null @@ -1,441 +0,0 @@ -// Copyright 2023 The Jujutsu Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// https://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use std::any::Any; -use std::fmt::Debug; -use std::io::Write as _; - -use clap::Subcommand; -use jj_lib::backend::TreeId; -use jj_lib::default_index::{AsCompositeIndex as _, DefaultIndexStore, DefaultReadonlyIndex}; -use jj_lib::local_working_copy::LocalWorkingCopy; -use jj_lib::merged_tree::MergedTree; -use jj_lib::object_id::ObjectId; -use jj_lib::repo::Repo; -use jj_lib::repo_path::RepoPathBuf; -use jj_lib::working_copy::WorkingCopy; -use jj_lib::{fileset, op_walk, revset}; - -use crate::cli_util::{CommandHelper, RevisionArg}; -use crate::command_error::{internal_error, user_error, CommandError}; -use crate::ui::Ui; -use crate::{revset_util, template_parser}; - -/// Low-level commands not intended for users -#[derive(Subcommand, Clone, Debug)] -#[command(hide = true)] -pub enum DebugCommand { - Fileset(DebugFilesetArgs), - Revset(DebugRevsetArgs), - #[command(name = "workingcopy")] - WorkingCopy(DebugWorkingCopyArgs), - Template(DebugTemplateArgs), - Index(DebugIndexArgs), - #[command(name = "reindex")] - ReIndex(DebugReIndexArgs), - #[command(visible_alias = "view")] - Operation(DebugOperationArgs), - Tree(DebugTreeArgs), - #[command(subcommand)] - Watchman(DebugWatchmanSubcommand), -} - -/// Parse fileset expression -#[derive(clap::Args, Clone, Debug)] -pub struct DebugFilesetArgs { - #[arg(value_hint = clap::ValueHint::AnyPath)] - path: String, -} - -/// Evaluate revset to full commit IDs -#[derive(clap::Args, Clone, Debug)] -pub struct DebugRevsetArgs { - revision: String, -} - -/// Show information about the working copy state -#[derive(clap::Args, Clone, Debug)] -pub struct DebugWorkingCopyArgs {} - -/// Parse a template -#[derive(clap::Args, Clone, Debug)] -pub struct DebugTemplateArgs { - template: String, -} - -/// Show commit index stats -#[derive(clap::Args, Clone, Debug)] -pub struct DebugIndexArgs {} - -/// Rebuild commit index -#[derive(clap::Args, Clone, Debug)] -pub struct DebugReIndexArgs {} - -/// Show information about an operation and its view -#[derive(clap::Args, Clone, Debug)] -pub struct DebugOperationArgs { - #[arg(default_value = "@")] - operation: String, - #[arg(long, value_enum, default_value = "all")] - display: DebugOperationDisplay, -} - -#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, clap::ValueEnum)] -pub enum DebugOperationDisplay { - /// Show only the operation details. - Operation, - /// Show the operation id only - Id, - /// Show only the view details - View, - /// Show both the view and the operation - All, -} - -/// List the recursive entries of a tree. -#[derive(clap::Args, Clone, Debug)] -pub struct DebugTreeArgs { - #[arg(long, short = 'r')] - revision: Option, - #[arg(long, conflicts_with = "revision")] - id: Option, - #[arg(long, requires = "id")] - dir: Option, - paths: Vec, - // TODO: Add an option to include trees that are ancestors of the matched paths -} - -#[derive(Subcommand, Clone, Debug)] -pub enum DebugWatchmanSubcommand { - /// Check whether `watchman` is enabled and whether it's correctly installed - Status, - QueryClock, - QueryChangedFiles, - ResetClock, -} - -pub fn cmd_debug( - ui: &mut Ui, - command: &CommandHelper, - subcommand: &DebugCommand, -) -> Result<(), CommandError> { - match subcommand { - DebugCommand::Fileset(args) => cmd_debug_fileset(ui, command, args), - DebugCommand::Revset(args) => cmd_debug_revset(ui, command, args), - DebugCommand::WorkingCopy(args) => cmd_debug_working_copy(ui, command, args), - DebugCommand::Template(args) => cmd_debug_template(ui, command, args), - DebugCommand::Index(args) => cmd_debug_index(ui, command, args), - DebugCommand::ReIndex(args) => cmd_debug_reindex(ui, command, args), - DebugCommand::Operation(args) => cmd_debug_operation(ui, command, args), - DebugCommand::Tree(args) => cmd_debug_tree(ui, command, args), - DebugCommand::Watchman(args) => cmd_debug_watchman(ui, command, args), - } -} - -fn cmd_debug_fileset( - ui: &mut Ui, - command: &CommandHelper, - args: &DebugFilesetArgs, -) -> Result<(), CommandError> { - let workspace_command = command.workspace_helper(ui)?; - let path_converter = workspace_command.path_converter(); - - let expression = fileset::parse_maybe_bare(&args.path, path_converter)?; - writeln!(ui.stdout(), "-- Parsed:")?; - writeln!(ui.stdout(), "{expression:#?}")?; - writeln!(ui.stdout())?; - - let matcher = expression.to_matcher(); - writeln!(ui.stdout(), "-- Matcher:")?; - writeln!(ui.stdout(), "{matcher:#?}")?; - Ok(()) -} - -fn cmd_debug_revset( - ui: &mut Ui, - command: &CommandHelper, - args: &DebugRevsetArgs, -) -> Result<(), CommandError> { - let workspace_command = command.workspace_helper(ui)?; - let workspace_ctx = workspace_command.revset_parse_context(); - let repo = workspace_command.repo().as_ref(); - - let expression = revset::parse(&args.revision, &workspace_ctx)?; - writeln!(ui.stdout(), "-- Parsed:")?; - writeln!(ui.stdout(), "{expression:#?}")?; - writeln!(ui.stdout())?; - - let expression = revset::optimize(expression); - writeln!(ui.stdout(), "-- Optimized:")?; - writeln!(ui.stdout(), "{expression:#?}")?; - writeln!(ui.stdout())?; - - let symbol_resolver = revset_util::default_symbol_resolver( - repo, - command.revset_extensions().symbol_resolvers(), - workspace_command.id_prefix_context()?, - ); - let expression = expression.resolve_user_expression(repo, &symbol_resolver)?; - writeln!(ui.stdout(), "-- Resolved:")?; - writeln!(ui.stdout(), "{expression:#?}")?; - writeln!(ui.stdout())?; - - let revset = expression.evaluate(repo)?; - writeln!(ui.stdout(), "-- Evaluated:")?; - writeln!(ui.stdout(), "{revset:#?}")?; - writeln!(ui.stdout())?; - - writeln!(ui.stdout(), "-- Commit IDs:")?; - for commit_id in revset.iter() { - writeln!(ui.stdout(), "{}", commit_id.hex())?; - } - Ok(()) -} - -fn cmd_debug_working_copy( - ui: &mut Ui, - command: &CommandHelper, - _args: &DebugWorkingCopyArgs, -) -> Result<(), CommandError> { - let workspace_command = command.workspace_helper(ui)?; - let wc = check_local_disk_wc(workspace_command.working_copy().as_any())?; - writeln!(ui.stdout(), "Current operation: {:?}", wc.operation_id())?; - writeln!(ui.stdout(), "Current tree: {:?}", wc.tree_id()?)?; - for (file, state) in wc.file_states()? { - writeln!( - ui.stdout(), - "{:?} {:13?} {:10?} {:?}", - state.file_type, - state.size, - state.mtime.0, - file - )?; - } - Ok(()) -} - -fn cmd_debug_template( - ui: &mut Ui, - _command: &CommandHelper, - args: &DebugTemplateArgs, -) -> Result<(), CommandError> { - let node = template_parser::parse_template(&args.template)?; - writeln!(ui.stdout(), "{node:#?}")?; - Ok(()) -} - -fn cmd_debug_index( - ui: &mut Ui, - command: &CommandHelper, - _args: &DebugIndexArgs, -) -> Result<(), CommandError> { - // Resolve the operation without loading the repo, so this command won't - // merge concurrent operations and update the index. - let workspace = command.load_workspace()?; - let repo_loader = workspace.repo_loader(); - let op = op_walk::resolve_op_for_load(repo_loader, &command.global_args().at_operation)?; - let index_store = repo_loader.index_store(); - let index = index_store - .get_index_at_op(&op, repo_loader.store()) - .map_err(internal_error)?; - if let Some(default_index) = index.as_any().downcast_ref::() { - let stats = default_index.as_composite().stats(); - writeln!(ui.stdout(), "Number of commits: {}", stats.num_commits)?; - writeln!(ui.stdout(), "Number of merges: {}", stats.num_merges)?; - writeln!( - ui.stdout(), - "Max generation number: {}", - stats.max_generation_number - )?; - writeln!(ui.stdout(), "Number of heads: {}", stats.num_heads)?; - writeln!(ui.stdout(), "Number of changes: {}", stats.num_changes)?; - writeln!(ui.stdout(), "Stats per level:")?; - for (i, level) in stats.levels.iter().enumerate() { - writeln!(ui.stdout(), " Level {i}:")?; - writeln!(ui.stdout(), " Number of commits: {}", level.num_commits)?; - writeln!(ui.stdout(), " Name: {}", level.name.as_ref().unwrap())?; - } - } else { - return Err(user_error(format!( - "Cannot get stats for indexes of type '{}'", - index_store.name() - ))); - } - Ok(()) -} - -fn cmd_debug_reindex( - ui: &mut Ui, - command: &CommandHelper, - _args: &DebugReIndexArgs, -) -> Result<(), CommandError> { - // Resolve the operation without loading the repo. The index might have to - // be rebuilt while loading the repo. - let workspace = command.load_workspace()?; - let repo_loader = workspace.repo_loader(); - let op = op_walk::resolve_op_for_load(repo_loader, &command.global_args().at_operation)?; - let index_store = repo_loader.index_store(); - if let Some(default_index_store) = index_store.as_any().downcast_ref::() { - default_index_store.reinit().map_err(internal_error)?; - let default_index = default_index_store - .build_index_at_operation(&op, repo_loader.store()) - .map_err(internal_error)?; - writeln!( - ui.status(), - "Finished indexing {:?} commits.", - default_index.as_composite().stats().num_commits - )?; - } else { - return Err(user_error(format!( - "Cannot reindex indexes of type '{}'", - index_store.name() - ))); - } - Ok(()) -} - -fn cmd_debug_operation( - ui: &mut Ui, - command: &CommandHelper, - args: &DebugOperationArgs, -) -> Result<(), CommandError> { - // Resolve the operation without loading the repo, so this command can be used - // even if e.g. the view object is broken. - let workspace = command.load_workspace()?; - let repo_loader = workspace.repo_loader(); - let op = op_walk::resolve_op_for_load(repo_loader, &args.operation)?; - if args.display == DebugOperationDisplay::Id { - writeln!(ui.stdout(), "{}", op.id().hex())?; - return Ok(()); - } - if args.display != DebugOperationDisplay::View { - writeln!(ui.stdout(), "{:#?}", op.store_operation())?; - } - if args.display != DebugOperationDisplay::Operation { - writeln!(ui.stdout(), "{:#?}", op.view()?.store_view())?; - } - Ok(()) -} - -fn cmd_debug_tree( - ui: &mut Ui, - command: &CommandHelper, - args: &DebugTreeArgs, -) -> Result<(), CommandError> { - let workspace_command = command.workspace_helper(ui)?; - let tree = if let Some(tree_id_hex) = &args.id { - let tree_id = - TreeId::try_from_hex(tree_id_hex).map_err(|_| user_error("Invalid tree id"))?; - let dir = if let Some(dir_str) = &args.dir { - workspace_command.parse_file_path(dir_str)? - } else { - RepoPathBuf::root() - }; - let store = workspace_command.repo().store(); - let tree = store.get_tree(&dir, &tree_id)?; - MergedTree::resolved(tree) - } else { - let commit = workspace_command - .resolve_single_rev(args.revision.as_ref().unwrap_or(&RevisionArg::AT))?; - commit.tree()? - }; - let matcher = workspace_command - .parse_file_patterns(&args.paths)? - .to_matcher(); - for (path, value) in tree.entries_matching(matcher.as_ref()) { - let ui_path = workspace_command.format_file_path(&path); - writeln!(ui.stdout(), "{ui_path}: {value:?}")?; - } - - Ok(()) -} - -#[cfg(feature = "watchman")] -fn cmd_debug_watchman( - ui: &mut Ui, - command: &CommandHelper, - subcommand: &DebugWatchmanSubcommand, -) -> Result<(), CommandError> { - use jj_lib::local_working_copy::LockedLocalWorkingCopy; - - let mut workspace_command = command.workspace_helper(ui)?; - let repo = workspace_command.repo().clone(); - match subcommand { - DebugWatchmanSubcommand::Status => { - // TODO(ilyagr): It would be nice to add colors here - match command.settings().fsmonitor_kind()? { - jj_lib::fsmonitor::FsmonitorKind::Watchman => { - writeln!(ui.stdout(), "Watchman is enabled via `core.fsmonitor`.")? - } - jj_lib::fsmonitor::FsmonitorKind::None => writeln!( - ui.stdout(), - "Watchman is disabled. Set `core.fsmonitor=\"watchman\"` to \ - enable.\nAttempting to contact the `watchman` CLI regardless..." - )?, - other_fsmonitor => { - return Err(user_error(format!( - "This command does not support the currently enabled filesystem monitor: \ - {other_fsmonitor:?}." - ))) - } - }; - let wc = check_local_disk_wc(workspace_command.working_copy().as_any())?; - let _ = wc.query_watchman()?; - writeln!( - ui.stdout(), - "The watchman server seems to be installed and working correctly." - )?; - } - DebugWatchmanSubcommand::QueryClock => { - let wc = check_local_disk_wc(workspace_command.working_copy().as_any())?; - let (clock, _changed_files) = wc.query_watchman()?; - writeln!(ui.stdout(), "Clock: {clock:?}")?; - } - DebugWatchmanSubcommand::QueryChangedFiles => { - let wc = check_local_disk_wc(workspace_command.working_copy().as_any())?; - let (_clock, changed_files) = wc.query_watchman()?; - writeln!(ui.stdout(), "Changed files: {changed_files:?}")?; - } - DebugWatchmanSubcommand::ResetClock => { - let (mut locked_ws, _commit) = workspace_command.start_working_copy_mutation()?; - let Some(locked_local_wc): Option<&mut LockedLocalWorkingCopy> = - locked_ws.locked_wc().as_any_mut().downcast_mut() - else { - return Err(user_error( - "This command requires a standard local-disk working copy", - )); - }; - locked_local_wc.reset_watchman()?; - locked_ws.finish(repo.op_id().clone())?; - writeln!(ui.status(), "Reset Watchman clock")?; - } - } - Ok(()) -} - -#[cfg(not(feature = "watchman"))] -fn cmd_debug_watchman( - _ui: &mut Ui, - _command: &CommandHelper, - _subcommand: &DebugWatchmanSubcommand, -) -> Result<(), CommandError> { - Err(user_error( - "Cannot query Watchman because jj was not compiled with the `watchman` feature", - )) -} - -fn check_local_disk_wc(x: &dyn Any) -> Result<&LocalWorkingCopy, CommandError> { - x.downcast_ref() - .ok_or_else(|| user_error("This command requires a standard local-disk working copy")) -} diff --git a/cli/src/commands/debug/fileset.rs b/cli/src/commands/debug/fileset.rs new file mode 100644 index 0000000000..2d0468a94f --- /dev/null +++ b/cli/src/commands/debug/fileset.rs @@ -0,0 +1,48 @@ +// Copyright 2023 The Jujutsu Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::fmt::Debug; +use std::io::Write as _; + +use jj_lib::fileset; + +use crate::cli_util::CommandHelper; +use crate::command_error::CommandError; +use crate::ui::Ui; + +/// Parse fileset expression +#[derive(clap::Args, Clone, Debug)] +pub struct DebugFilesetArgs { + #[arg(value_hint = clap::ValueHint::AnyPath)] + path: String, +} + +pub fn cmd_debug_fileset( + ui: &mut Ui, + command: &CommandHelper, + args: &DebugFilesetArgs, +) -> Result<(), CommandError> { + let workspace_command = command.workspace_helper(ui)?; + let path_converter = workspace_command.path_converter(); + + let expression = fileset::parse_maybe_bare(&args.path, path_converter)?; + writeln!(ui.stdout(), "-- Parsed:")?; + writeln!(ui.stdout(), "{expression:#?}")?; + writeln!(ui.stdout())?; + + let matcher = expression.to_matcher(); + writeln!(ui.stdout(), "-- Matcher:")?; + writeln!(ui.stdout(), "{matcher:#?}")?; + Ok(()) +} diff --git a/cli/src/commands/debug/index.rs b/cli/src/commands/debug/index.rs new file mode 100644 index 0000000000..2c9eb06a68 --- /dev/null +++ b/cli/src/commands/debug/index.rs @@ -0,0 +1,67 @@ +// Copyright 2023 The Jujutsu Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::fmt::Debug; +use std::io::Write as _; + +use jj_lib::default_index::{AsCompositeIndex as _, DefaultReadonlyIndex}; +use jj_lib::op_walk; + +use crate::cli_util::CommandHelper; +use crate::command_error::{internal_error, user_error, CommandError}; +use crate::ui::Ui; + +/// Show commit index stats +#[derive(clap::Args, Clone, Debug)] +pub struct DebugIndexArgs {} + +pub fn cmd_debug_index( + ui: &mut Ui, + command: &CommandHelper, + _args: &DebugIndexArgs, +) -> Result<(), CommandError> { + // Resolve the operation without loading the repo, so this command won't + // merge concurrent operations and update the index. + let workspace = command.load_workspace()?; + let repo_loader = workspace.repo_loader(); + let op = op_walk::resolve_op_for_load(repo_loader, &command.global_args().at_operation)?; + let index_store = repo_loader.index_store(); + let index = index_store + .get_index_at_op(&op, repo_loader.store()) + .map_err(internal_error)?; + if let Some(default_index) = index.as_any().downcast_ref::() { + let stats = default_index.as_composite().stats(); + writeln!(ui.stdout(), "Number of commits: {}", stats.num_commits)?; + writeln!(ui.stdout(), "Number of merges: {}", stats.num_merges)?; + writeln!( + ui.stdout(), + "Max generation number: {}", + stats.max_generation_number + )?; + writeln!(ui.stdout(), "Number of heads: {}", stats.num_heads)?; + writeln!(ui.stdout(), "Number of changes: {}", stats.num_changes)?; + writeln!(ui.stdout(), "Stats per level:")?; + for (i, level) in stats.levels.iter().enumerate() { + writeln!(ui.stdout(), " Level {i}:")?; + writeln!(ui.stdout(), " Number of commits: {}", level.num_commits)?; + writeln!(ui.stdout(), " Name: {}", level.name.as_ref().unwrap())?; + } + } else { + return Err(user_error(format!( + "Cannot get stats for indexes of type '{}'", + index_store.name() + ))); + } + Ok(()) +} diff --git a/cli/src/commands/debug/local_working_copy.rs b/cli/src/commands/debug/local_working_copy.rs new file mode 100644 index 0000000000..98408799b2 --- /dev/null +++ b/cli/src/commands/debug/local_working_copy.rs @@ -0,0 +1,51 @@ +// Copyright 2023 The Jujutsu Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::fmt::Debug; +use std::io::Write as _; + +use jj_lib::working_copy::WorkingCopy; + +use super::check_local_disk_wc; +use crate::cli_util::CommandHelper; +use crate::command_error::CommandError; +use crate::ui::Ui; + +/// Show information about the local working copy state +/// +/// This command only works with a standard local-disk working copy. +#[derive(clap::Args, Clone, Debug)] +pub struct DebugLocalWorkingCopyArgs {} + +pub fn cmd_debug_local_working_copy( + ui: &mut Ui, + command: &CommandHelper, + _args: &DebugLocalWorkingCopyArgs, +) -> Result<(), CommandError> { + let workspace_command = command.workspace_helper(ui)?; + let wc = check_local_disk_wc(workspace_command.working_copy().as_any())?; + writeln!(ui.stdout(), "Current operation: {:?}", wc.operation_id())?; + writeln!(ui.stdout(), "Current tree: {:?}", wc.tree_id()?)?; + for (file, state) in wc.file_states()? { + writeln!( + ui.stdout(), + "{:?} {:13?} {:10?} {:?}", + state.file_type, + state.size, + state.mtime.0, + file + )?; + } + Ok(()) +} diff --git a/cli/src/commands/debug/mod.rs b/cli/src/commands/debug/mod.rs new file mode 100644 index 0000000000..97138e2faf --- /dev/null +++ b/cli/src/commands/debug/mod.rs @@ -0,0 +1,90 @@ +// Copyright 2023 The Jujutsu Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +pub mod fileset; +pub mod index; +pub mod local_working_copy; +pub mod operation; +pub mod reindex; +pub mod revset; +pub mod snapshot; +pub mod template; +pub mod tree; +pub mod watchman; +pub mod working_copy; + +use std::any::Any; +use std::fmt::Debug; + +use clap::Subcommand; +use jj_lib::local_working_copy::LocalWorkingCopy; + +use self::fileset::{cmd_debug_fileset, DebugFilesetArgs}; +use self::index::{cmd_debug_index, DebugIndexArgs}; +use self::local_working_copy::{cmd_debug_local_working_copy, DebugLocalWorkingCopyArgs}; +use self::operation::{cmd_debug_operation, DebugOperationArgs}; +use self::reindex::{cmd_debug_reindex, DebugReindexArgs}; +use self::revset::{cmd_debug_revset, DebugRevsetArgs}; +use self::snapshot::{cmd_debug_snapshot, DebugSnapshotArgs}; +use self::template::{cmd_debug_template, DebugTemplateArgs}; +use self::tree::{cmd_debug_tree, DebugTreeArgs}; +use self::watchman::{cmd_debug_watchman, DebugWatchmanCommand}; +use self::working_copy::{cmd_debug_working_copy, DebugWorkingCopyArgs}; +use crate::cli_util::CommandHelper; +use crate::command_error::{user_error, CommandError}; +use crate::ui::Ui; + +/// Low-level commands not intended for users +#[derive(Subcommand, Clone, Debug)] +#[command(hide = true)] +pub enum DebugCommand { + Fileset(DebugFilesetArgs), + Index(DebugIndexArgs), + LocalWorkingCopy(DebugLocalWorkingCopyArgs), + #[command(visible_alias = "view")] + Operation(DebugOperationArgs), + Reindex(DebugReindexArgs), + Revset(DebugRevsetArgs), + Snapshot(DebugSnapshotArgs), + Template(DebugTemplateArgs), + Tree(DebugTreeArgs), + #[command(subcommand)] + Watchman(DebugWatchmanCommand), + WorkingCopy(DebugWorkingCopyArgs), +} + +pub fn cmd_debug( + ui: &mut Ui, + command: &CommandHelper, + subcommand: &DebugCommand, +) -> Result<(), CommandError> { + match subcommand { + DebugCommand::Fileset(args) => cmd_debug_fileset(ui, command, args), + DebugCommand::Index(args) => cmd_debug_index(ui, command, args), + DebugCommand::LocalWorkingCopy(args) => cmd_debug_local_working_copy(ui, command, args), + DebugCommand::Operation(args) => cmd_debug_operation(ui, command, args), + DebugCommand::Reindex(args) => cmd_debug_reindex(ui, command, args), + DebugCommand::Revset(args) => cmd_debug_revset(ui, command, args), + DebugCommand::Snapshot(args) => cmd_debug_snapshot(ui, command, args), + DebugCommand::Template(args) => cmd_debug_template(ui, command, args), + DebugCommand::Tree(args) => cmd_debug_tree(ui, command, args), + DebugCommand::Watchman(args) => cmd_debug_watchman(ui, command, args), + DebugCommand::WorkingCopy(args) => cmd_debug_working_copy(ui, command, args), + } +} + +fn check_local_disk_wc(x: &dyn Any) -> Result<&LocalWorkingCopy, CommandError> { + x.downcast_ref() + .ok_or_else(|| user_error("This command requires a standard local-disk working copy")) +} diff --git a/cli/src/commands/debug/operation.rs b/cli/src/commands/debug/operation.rs new file mode 100644 index 0000000000..ccf3fdec53 --- /dev/null +++ b/cli/src/commands/debug/operation.rs @@ -0,0 +1,67 @@ +// Copyright 2023 The Jujutsu Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::fmt::Debug; +use std::io::Write as _; + +use jj_lib::object_id::ObjectId; +use jj_lib::op_walk; + +use crate::cli_util::CommandHelper; +use crate::command_error::CommandError; +use crate::ui::Ui; + +/// Show information about an operation and its view +#[derive(clap::Args, Clone, Debug)] +pub struct DebugOperationArgs { + #[arg(default_value = "@")] + operation: String, + #[arg(long, value_enum, default_value = "all")] + display: OperationDisplay, +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, clap::ValueEnum)] +pub enum OperationDisplay { + /// Show only the operation details. + Operation, + /// Show the operation id only + Id, + /// Show only the view details + View, + /// Show both the view and the operation + All, +} + +pub fn cmd_debug_operation( + ui: &mut Ui, + command: &CommandHelper, + args: &DebugOperationArgs, +) -> Result<(), CommandError> { + // Resolve the operation without loading the repo, so this command can be used + // even if e.g. the view object is broken. + let workspace = command.load_workspace()?; + let repo_loader = workspace.repo_loader(); + let op = op_walk::resolve_op_for_load(repo_loader, &args.operation)?; + if args.display == OperationDisplay::Id { + writeln!(ui.stdout(), "{}", op.id().hex())?; + return Ok(()); + } + if args.display != OperationDisplay::View { + writeln!(ui.stdout(), "{:#?}", op.store_operation())?; + } + if args.display != OperationDisplay::Operation { + writeln!(ui.stdout(), "{:#?}", op.view()?.store_view())?; + } + Ok(()) +} diff --git a/cli/src/commands/debug/reindex.rs b/cli/src/commands/debug/reindex.rs new file mode 100644 index 0000000000..02ecc6f009 --- /dev/null +++ b/cli/src/commands/debug/reindex.rs @@ -0,0 +1,57 @@ +// Copyright 2023 The Jujutsu Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::fmt::Debug; +use std::io::Write as _; + +use jj_lib::default_index::{AsCompositeIndex as _, DefaultIndexStore}; +use jj_lib::op_walk; + +use crate::cli_util::CommandHelper; +use crate::command_error::{internal_error, user_error, CommandError}; +use crate::ui::Ui; + +/// Rebuild commit index +#[derive(clap::Args, Clone, Debug)] +pub struct DebugReindexArgs {} + +pub fn cmd_debug_reindex( + ui: &mut Ui, + command: &CommandHelper, + _args: &DebugReindexArgs, +) -> Result<(), CommandError> { + // Resolve the operation without loading the repo. The index might have to + // be rebuilt while loading the repo. + let workspace = command.load_workspace()?; + let repo_loader = workspace.repo_loader(); + let op = op_walk::resolve_op_for_load(repo_loader, &command.global_args().at_operation)?; + let index_store = repo_loader.index_store(); + if let Some(default_index_store) = index_store.as_any().downcast_ref::() { + default_index_store.reinit().map_err(internal_error)?; + let default_index = default_index_store + .build_index_at_operation(&op, repo_loader.store()) + .map_err(internal_error)?; + writeln!( + ui.status(), + "Finished indexing {:?} commits.", + default_index.as_composite().stats().num_commits + )?; + } else { + return Err(user_error(format!( + "Cannot reindex indexes of type '{}'", + index_store.name() + ))); + } + Ok(()) +} diff --git a/cli/src/commands/debug/revset.rs b/cli/src/commands/debug/revset.rs new file mode 100644 index 0000000000..5a91d36c08 --- /dev/null +++ b/cli/src/commands/debug/revset.rs @@ -0,0 +1,71 @@ +// Copyright 2023 The Jujutsu Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::fmt::Debug; +use std::io::Write as _; + +use jj_lib::object_id::ObjectId; +use jj_lib::revset; + +use crate::cli_util::CommandHelper; +use crate::command_error::CommandError; +use crate::revset_util; +use crate::ui::Ui; + +/// Evaluate revset to full commit IDs +#[derive(clap::Args, Clone, Debug)] +pub struct DebugRevsetArgs { + revision: String, +} + +pub fn cmd_debug_revset( + ui: &mut Ui, + command: &CommandHelper, + args: &DebugRevsetArgs, +) -> Result<(), CommandError> { + let workspace_command = command.workspace_helper(ui)?; + let workspace_ctx = workspace_command.revset_parse_context(); + let repo = workspace_command.repo().as_ref(); + + let expression = revset::parse(&args.revision, &workspace_ctx)?; + writeln!(ui.stdout(), "-- Parsed:")?; + writeln!(ui.stdout(), "{expression:#?}")?; + writeln!(ui.stdout())?; + + let expression = revset::optimize(expression); + writeln!(ui.stdout(), "-- Optimized:")?; + writeln!(ui.stdout(), "{expression:#?}")?; + writeln!(ui.stdout())?; + + let symbol_resolver = revset_util::default_symbol_resolver( + repo, + command.revset_extensions().symbol_resolvers(), + workspace_command.id_prefix_context()?, + ); + let expression = expression.resolve_user_expression(repo, &symbol_resolver)?; + writeln!(ui.stdout(), "-- Resolved:")?; + writeln!(ui.stdout(), "{expression:#?}")?; + writeln!(ui.stdout())?; + + let revset = expression.evaluate(repo)?; + writeln!(ui.stdout(), "-- Evaluated:")?; + writeln!(ui.stdout(), "{revset:#?}")?; + writeln!(ui.stdout())?; + + writeln!(ui.stdout(), "-- Commit IDs:")?; + for commit_id in revset.iter() { + writeln!(ui.stdout(), "{}", commit_id.hex())?; + } + Ok(()) +} diff --git a/cli/src/commands/debug/snapshot.rs b/cli/src/commands/debug/snapshot.rs new file mode 100644 index 0000000000..629129da67 --- /dev/null +++ b/cli/src/commands/debug/snapshot.rs @@ -0,0 +1,33 @@ +// Copyright 2024 The Jujutsu Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::fmt::Debug; + +use crate::cli_util::CommandHelper; +use crate::command_error::CommandError; +use crate::ui::Ui; + +/// Trigger a snapshot in the op log +#[derive(clap::Args, Clone, Debug)] +pub struct DebugSnapshotArgs {} + +pub fn cmd_debug_snapshot( + ui: &mut Ui, + command: &CommandHelper, + _args: &DebugSnapshotArgs, +) -> Result<(), CommandError> { + // workspace helper will snapshot as needed + command.workspace_helper(ui)?; + Ok(()) +} diff --git a/cli/src/commands/debug/template.rs b/cli/src/commands/debug/template.rs new file mode 100644 index 0000000000..1af2f4aeda --- /dev/null +++ b/cli/src/commands/debug/template.rs @@ -0,0 +1,37 @@ +// Copyright 2023 The Jujutsu Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::fmt::Debug; +use std::io::Write as _; + +use crate::cli_util::CommandHelper; +use crate::command_error::CommandError; +use crate::template_parser; +use crate::ui::Ui; + +/// Parse a template +#[derive(clap::Args, Clone, Debug)] +pub struct DebugTemplateArgs { + template: String, +} + +pub fn cmd_debug_template( + ui: &mut Ui, + _command: &CommandHelper, + args: &DebugTemplateArgs, +) -> Result<(), CommandError> { + let node = template_parser::parse_template(&args.template)?; + writeln!(ui.stdout(), "{node:#?}")?; + Ok(()) +} diff --git a/cli/src/commands/debug/tree.rs b/cli/src/commands/debug/tree.rs new file mode 100644 index 0000000000..17ee28aa55 --- /dev/null +++ b/cli/src/commands/debug/tree.rs @@ -0,0 +1,71 @@ +// Copyright 2023 The Jujutsu Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::fmt::Debug; +use std::io::Write as _; + +use jj_lib::backend::TreeId; +use jj_lib::merged_tree::MergedTree; +use jj_lib::repo::Repo; +use jj_lib::repo_path::RepoPathBuf; + +use crate::cli_util::{CommandHelper, RevisionArg}; +use crate::command_error::{user_error, CommandError}; +use crate::ui::Ui; + +/// List the recursive entries of a tree. +#[derive(clap::Args, Clone, Debug)] +pub struct DebugTreeArgs { + #[arg(long, short = 'r')] + revision: Option, + #[arg(long, conflicts_with = "revision")] + id: Option, + #[arg(long, requires = "id")] + dir: Option, + paths: Vec, + // TODO: Add an option to include trees that are ancestors of the matched paths +} + +pub fn cmd_debug_tree( + ui: &mut Ui, + command: &CommandHelper, + args: &DebugTreeArgs, +) -> Result<(), CommandError> { + let workspace_command = command.workspace_helper(ui)?; + let tree = if let Some(tree_id_hex) = &args.id { + let tree_id = + TreeId::try_from_hex(tree_id_hex).map_err(|_| user_error("Invalid tree id"))?; + let dir = if let Some(dir_str) = &args.dir { + workspace_command.parse_file_path(dir_str)? + } else { + RepoPathBuf::root() + }; + let store = workspace_command.repo().store(); + let tree = store.get_tree(&dir, &tree_id)?; + MergedTree::resolved(tree) + } else { + let commit = workspace_command + .resolve_single_rev(args.revision.as_ref().unwrap_or(&RevisionArg::AT))?; + commit.tree()? + }; + let matcher = workspace_command + .parse_file_patterns(&args.paths)? + .to_matcher(); + for (path, value) in tree.entries_matching(matcher.as_ref()) { + let ui_path = workspace_command.format_file_path(&path); + writeln!(ui.stdout(), "{ui_path}: {value:?}")?; + } + + Ok(()) +} diff --git a/cli/src/commands/debug/watchman.rs b/cli/src/commands/debug/watchman.rs new file mode 100644 index 0000000000..322cf8743d --- /dev/null +++ b/cli/src/commands/debug/watchman.rs @@ -0,0 +1,137 @@ +// Copyright 2023 The Jujutsu Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::any::Any; +use std::fmt::Debug; +use std::io::Write as _; + +use clap::Subcommand; +use jj_lib::fsmonitor::{FsmonitorSettings, WatchmanConfig}; +use jj_lib::local_working_copy::LocalWorkingCopy; + +use crate::cli_util::CommandHelper; +use crate::command_error::{user_error, CommandError}; +use crate::ui::Ui; + +#[derive(Subcommand, Clone, Debug)] +pub enum DebugWatchmanCommand { + /// Check whether `watchman` is enabled and whether it's correctly installed + Status, + QueryClock, + QueryChangedFiles, + ResetClock, +} + +#[cfg(feature = "watchman")] +pub fn cmd_debug_watchman( + ui: &mut Ui, + command: &CommandHelper, + subcommand: &DebugWatchmanCommand, +) -> Result<(), CommandError> { + use jj_lib::local_working_copy::LockedLocalWorkingCopy; + + let mut workspace_command = command.workspace_helper(ui)?; + let repo = workspace_command.repo().clone(); + match subcommand { + DebugWatchmanCommand::Status => { + // TODO(ilyagr): It would be nice to add colors here + let config = match command.settings().fsmonitor_settings()? { + FsmonitorSettings::Watchman(config) => { + writeln!(ui.stdout(), "Watchman is enabled via `core.fsmonitor`.")?; + writeln!( + ui.stdout(), + r"Background snapshotting is {}. Use `core.watchman.register_snapshot_trigger` to control it.", + if config.register_trigger { + "enabled" + } else { + "disabled" + } + )?; + config + } + FsmonitorSettings::None => { + writeln!( + ui.stdout(), + r#"Watchman is disabled. Set `core.fsmonitor="watchman"` to enable."# + )?; + writeln!( + ui.stdout(), + "Attempting to contact the `watchman` CLI regardless..." + )?; + WatchmanConfig::default() + } + other_fsmonitor => { + return Err(user_error(format!( + r"This command does not support the currently enabled filesystem monitor: {other_fsmonitor:?}." + ))) + } + }; + let wc = check_local_disk_wc(workspace_command.working_copy().as_any())?; + let _ = wc.query_watchman(&config)?; + writeln!( + ui.stdout(), + "The watchman server seems to be installed and working correctly." + )?; + writeln!( + ui.stdout(), + "Background snapshotting is currently {}.", + if wc.is_watchman_trigger_registered(&config)? { + "active" + } else { + "inactive" + } + )?; + } + DebugWatchmanCommand::QueryClock => { + let wc = check_local_disk_wc(workspace_command.working_copy().as_any())?; + let (clock, _changed_files) = wc.query_watchman(&WatchmanConfig::default())?; + writeln!(ui.stdout(), "Clock: {clock:?}")?; + } + DebugWatchmanCommand::QueryChangedFiles => { + let wc = check_local_disk_wc(workspace_command.working_copy().as_any())?; + let (_clock, changed_files) = wc.query_watchman(&WatchmanConfig::default())?; + writeln!(ui.stdout(), "Changed files: {changed_files:?}")?; + } + DebugWatchmanCommand::ResetClock => { + let (mut locked_ws, _commit) = workspace_command.start_working_copy_mutation()?; + let Some(locked_local_wc): Option<&mut LockedLocalWorkingCopy> = + locked_ws.locked_wc().as_any_mut().downcast_mut() + else { + return Err(user_error( + "This command requires a standard local-disk working copy", + )); + }; + locked_local_wc.reset_watchman()?; + locked_ws.finish(repo.op_id().clone())?; + writeln!(ui.status(), "Reset Watchman clock")?; + } + } + Ok(()) +} + +#[cfg(not(feature = "watchman"))] +pub fn cmd_debug_watchman( + _ui: &mut Ui, + _command: &CommandHelper, + _subcommand: &DebugWatchmanCommand, +) -> Result<(), CommandError> { + Err(user_error( + "Cannot query Watchman because jj was not compiled with the `watchman` feature", + )) +} + +fn check_local_disk_wc(x: &dyn Any) -> Result<&LocalWorkingCopy, CommandError> { + x.downcast_ref() + .ok_or_else(|| user_error("This command requires a standard local-disk working copy")) +} diff --git a/cli/src/commands/debug/working_copy.rs b/cli/src/commands/debug/working_copy.rs new file mode 100644 index 0000000000..0cf42c537e --- /dev/null +++ b/cli/src/commands/debug/working_copy.rs @@ -0,0 +1,37 @@ +// Copyright 2023 The Jujutsu Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::fmt::Debug; +use std::io::Write as _; + +use crate::cli_util::CommandHelper; +use crate::command_error::CommandError; +use crate::ui::Ui; + +/// Show information about the working copy state +#[derive(clap::Args, Clone, Debug)] +pub struct DebugWorkingCopyArgs {} + +pub fn cmd_debug_working_copy( + ui: &mut Ui, + command: &CommandHelper, + _args: &DebugWorkingCopyArgs, +) -> Result<(), CommandError> { + let workspace_command = command.workspace_helper_no_snapshot(ui)?; + let wc = workspace_command.working_copy(); + writeln!(ui.stdout(), "Type: {:?}", wc.name())?; + writeln!(ui.stdout(), "Current operation: {:?}", wc.operation_id())?; + writeln!(ui.stdout(), "Current tree: {:?}", wc.tree_id()?)?; + Ok(()) +} diff --git a/cli/src/commands/chmod.rs b/cli/src/commands/file/chmod.rs similarity index 86% rename from cli/src/commands/chmod.rs rename to cli/src/commands/file/chmod.rs index 1bc29aa05c..bf4b844692 100644 --- a/cli/src/commands/chmod.rs +++ b/cli/src/commands/file/chmod.rs @@ -34,10 +34,10 @@ enum ChmodMode { /// Sets or removes the executable bit for paths in the repo /// -/// Unlike the POSIX `chmod`, `jj chmod` also works on Windows, on conflicted -/// files, and on arbitrary revisions. +/// Unlike the POSIX `chmod`, `jj file chmod` also works on Windows, on +/// conflicted files, and on arbitrary revisions. #[derive(clap::Args, Clone, Debug)] -pub(crate) struct ChmodArgs { +pub(crate) struct FileChmodArgs { mode: ChmodMode, /// The revision to update #[arg(long, short, default_value = "@")] @@ -48,10 +48,27 @@ pub(crate) struct ChmodArgs { } #[instrument(skip_all)] -pub(crate) fn cmd_chmod( +pub(crate) fn deprecated_cmd_chmod( ui: &mut Ui, command: &CommandHelper, - args: &ChmodArgs, + args: &FileChmodArgs, +) -> Result<(), CommandError> { + writeln!( + ui.warning_default(), + "`jj chmod` is deprecated; use `jj file chmod` instead, which is equivalent" + )?; + writeln!( + ui.warning_default(), + "`jj chmod` will be removed in a future version, and this will be a hard error" + )?; + cmd_file_chmod(ui, command, args) +} + +#[instrument(skip_all)] +pub(crate) fn cmd_file_chmod( + ui: &mut Ui, + command: &CommandHelper, + args: &FileChmodArgs, ) -> Result<(), CommandError> { let executable_bit = match args.mode { ChmodMode::Executable => true, diff --git a/cli/src/commands/files.rs b/cli/src/commands/file/list.rs similarity index 74% rename from cli/src/commands/files.rs rename to cli/src/commands/file/list.rs index 5b836795ae..e0d2b08263 100644 --- a/cli/src/commands/files.rs +++ b/cli/src/commands/file/list.rs @@ -22,7 +22,7 @@ use crate::ui::Ui; /// List files in a revision #[derive(clap::Args, Clone, Debug)] -pub(crate) struct FilesArgs { +pub(crate) struct FileListArgs { /// The revision to list files in #[arg(long, short, default_value = "@")] revision: RevisionArg, @@ -32,10 +32,27 @@ pub(crate) struct FilesArgs { } #[instrument(skip_all)] -pub(crate) fn cmd_files( +pub(crate) fn deprecated_cmd_files( ui: &mut Ui, command: &CommandHelper, - args: &FilesArgs, + args: &FileListArgs, +) -> Result<(), CommandError> { + writeln!( + ui.warning_default(), + "`jj files` is deprecated; use `jj file list` instead, which is equivalent" + )?; + writeln!( + ui.warning_default(), + "`jj files` will be removed in a future version, and this will be a hard error" + )?; + cmd_file_list(ui, command, args) +} + +#[instrument(skip_all)] +pub(crate) fn cmd_file_list( + ui: &mut Ui, + command: &CommandHelper, + args: &FileListArgs, ) -> Result<(), CommandError> { let workspace_command = command.workspace_helper(ui)?; let commit = workspace_command.resolve_single_rev(&args.revision)?; diff --git a/cli/src/commands/file/mod.rs b/cli/src/commands/file/mod.rs new file mode 100644 index 0000000000..b642c31125 --- /dev/null +++ b/cli/src/commands/file/mod.rs @@ -0,0 +1,41 @@ +// Copyright 2020-2023 The Jujutsu Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +pub mod chmod; +pub mod list; +pub mod show; + +use crate::cli_util::CommandHelper; +use crate::command_error::CommandError; +use crate::ui::Ui; + +/// File operations. +#[derive(clap::Subcommand, Clone, Debug)] +pub enum FileCommand { + Chmod(chmod::FileChmodArgs), + List(list::FileListArgs), + Show(show::FileShowArgs), +} + +pub fn cmd_file( + ui: &mut Ui, + command: &CommandHelper, + subcommand: &FileCommand, +) -> Result<(), CommandError> { + match subcommand { + FileCommand::Chmod(args) => chmod::cmd_file_chmod(ui, command, args), + FileCommand::List(args) => list::cmd_file_list(ui, command, args), + FileCommand::Show(args) => show::cmd_file_show(ui, command, args), + } +} diff --git a/cli/src/commands/cat.rs b/cli/src/commands/file/show.rs similarity index 89% rename from cli/src/commands/cat.rs rename to cli/src/commands/file/show.rs index 8e5d8a4acd..f99f26bf1e 100644 --- a/cli/src/commands/cat.rs +++ b/cli/src/commands/file/show.rs @@ -34,7 +34,7 @@ use crate::ui::Ui; /// If the given path is a directory, files in the directory will be visited /// recursively. #[derive(clap::Args, Clone, Debug)] -pub(crate) struct CatArgs { +pub(crate) struct FileShowArgs { /// The revision to get the file contents from #[arg(long, short, default_value = "@")] revision: RevisionArg, @@ -44,10 +44,27 @@ pub(crate) struct CatArgs { } #[instrument(skip_all)] -pub(crate) fn cmd_cat( +pub(crate) fn deprecated_cmd_cat( ui: &mut Ui, command: &CommandHelper, - args: &CatArgs, + args: &FileShowArgs, +) -> Result<(), CommandError> { + writeln!( + ui.warning_default(), + "`jj cat` is deprecated; use `jj file show` instead, which is equivalent" + )?; + writeln!( + ui.warning_default(), + "`jj cat` will be removed in a future version, and this will be a hard error" + )?; + cmd_file_show(ui, command, args) +} + +#[instrument(skip_all)] +pub(crate) fn cmd_file_show( + ui: &mut Ui, + command: &CommandHelper, + args: &FileShowArgs, ) -> Result<(), CommandError> { let workspace_command = command.workspace_helper(ui)?; let commit = workspace_command.resolve_single_rev(&args.revision)?; diff --git a/cli/src/commands/git.rs b/cli/src/commands/git.rs deleted file mode 100644 index 66d288286a..0000000000 --- a/cli/src/commands/git.rs +++ /dev/null @@ -1,1350 +0,0 @@ -// Copyright 2020-2023 The Jujutsu Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// https://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use std::collections::{HashMap, HashSet}; -use std::io::Write; -use std::path::{Path, PathBuf}; -use std::sync::Arc; -use std::{fmt, fs, io}; - -use clap::{ArgGroup, Subcommand}; -use itertools::Itertools; -use jj_lib::backend::TreeValue; -use jj_lib::file_util; -use jj_lib::git::{ - self, parse_gitmodules, GitBranchPushTargets, GitFetchError, GitFetchStats, GitPushError, -}; -use jj_lib::object_id::ObjectId; -use jj_lib::op_store::RefTarget; -use jj_lib::refs::{ - classify_branch_push_action, BranchPushAction, BranchPushUpdate, LocalAndRemoteRef, -}; -use jj_lib::repo::{ReadonlyRepo, Repo}; -use jj_lib::repo_path::RepoPath; -use jj_lib::revset::{self, RevsetExpression, RevsetIteratorExt as _}; -use jj_lib::settings::{ConfigResultExt as _, UserSettings}; -use jj_lib::str_util::StringPattern; -use jj_lib::view::View; -use jj_lib::workspace::Workspace; - -use crate::cli_util::{ - print_trackable_remote_branches, short_change_hash, short_commit_hash, start_repo_transaction, - CommandHelper, RevisionArg, WorkspaceCommandHelper, WorkspaceCommandTransaction, -}; -use crate::command_error::{ - user_error, user_error_with_hint, user_error_with_message, CommandError, -}; -use crate::git_util::{ - get_git_repo, is_colocated_git_workspace, print_failed_git_export, print_git_import_stats, - with_remote_git_callbacks, GitSidebandProgressMessageWriter, -}; -use crate::ui::Ui; - -/// Commands for working with Git remotes and the underlying Git repo -/// -/// For a comparison with Git, including a table of commands, see -/// https://github.com/martinvonz/jj/blob/main/docs/git-comparison.md. -#[derive(Subcommand, Clone, Debug)] -pub enum GitCommand { - #[command(subcommand)] - Remote(GitRemoteCommand), - Init(GitInitArgs), - Fetch(GitFetchArgs), - Clone(GitCloneArgs), - Push(GitPushArgs), - Import(GitImportArgs), - Export(GitExportArgs), - #[command(subcommand, hide = true)] - Submodule(GitSubmoduleCommand), -} - -/// Manage Git remotes -/// -/// The Git repo will be a bare git repo stored inside the `.jj/` directory. -#[derive(Subcommand, Clone, Debug)] -pub enum GitRemoteCommand { - Add(GitRemoteAddArgs), - Remove(GitRemoteRemoveArgs), - Rename(GitRemoteRenameArgs), - List(GitRemoteListArgs), -} - -/// Add a Git remote -#[derive(clap::Args, Clone, Debug)] -pub struct GitRemoteAddArgs { - /// The remote's name - remote: String, - /// The remote's URL - url: String, -} - -/// Remove a Git remote and forget its branches -#[derive(clap::Args, Clone, Debug)] -pub struct GitRemoteRemoveArgs { - /// The remote's name - remote: String, -} - -/// Rename a Git remote -#[derive(clap::Args, Clone, Debug)] -pub struct GitRemoteRenameArgs { - /// The name of an existing remote - old: String, - /// The desired name for `old` - new: String, -} - -/// List Git remotes -#[derive(clap::Args, Clone, Debug)] -pub struct GitRemoteListArgs {} - -/// Create a new Git backed repo. -#[derive(clap::Args, Clone, Debug)] -pub struct GitInitArgs { - /// The destination directory where the `jj` repo will be created. - /// If the directory does not exist, it will be created. - /// If no directory is given, the current directory is used. - /// - /// By default the `git` repo is under `$destination/.jj` - #[arg(default_value = ".", value_hint = clap::ValueHint::DirPath)] - destination: String, - - /// Specifies that the `jj` repo should also be a valid - /// `git` repo, allowing the use of both `jj` and `git` commands - /// in the same directory. - /// - /// This is done by placing the backing git repo into a `.git` directory in - /// the root of the `jj` repo along with the `.jj` directory. If the `.git` - /// directory already exists, all the existing commits will be imported. - /// - /// This option is mutually exclusive with `--git-repo`. - #[arg(long, conflicts_with = "git_repo")] - colocate: bool, - - /// Specifies a path to an **existing** git repository to be - /// used as the backing git repo for the newly created `jj` repo. - /// - /// If the specified `--git-repo` path happens to be the same as - /// the `jj` repo path (both .jj and .git directories are in the - /// same working directory), then both `jj` and `git` commands - /// will work on the same repo. This is called a co-located repo. - /// - /// This option is mutually exclusive with `--colocate`. - #[arg(long, conflicts_with = "colocate", value_hint = clap::ValueHint::DirPath)] - git_repo: Option, -} - -/// Fetch from a Git remote -/// -/// If a working-copy commit gets abandoned, it will be given a new, empty -/// commit. This is true in general; it is not specific to this command. -#[derive(clap::Args, Clone, Debug)] -pub struct GitFetchArgs { - /// Fetch only some of the branches - /// - /// By default, the specified name matches exactly. Use `glob:` prefix to - /// expand `*` as a glob. The other wildcard characters aren't supported. - #[arg(long, short, default_value = "glob:*", value_parser = StringPattern::parse)] - branch: Vec, - /// The remote to fetch from (only named remotes are supported, can be - /// repeated) - #[arg(long = "remote", value_name = "remote")] - remotes: Vec, - /// Fetch from all remotes - #[arg(long, conflicts_with = "remotes")] - all_remotes: bool, -} - -/// Create a new repo backed by a clone of a Git repo -/// -/// The Git repo will be a bare git repo stored inside the `.jj/` directory. -#[derive(clap::Args, Clone, Debug)] -pub struct GitCloneArgs { - /// URL or path of the Git repo to clone - #[arg(value_hint = clap::ValueHint::DirPath)] - source: String, - /// The directory to write the Jujutsu repo to - #[arg(value_hint = clap::ValueHint::DirPath)] - destination: Option, - /// Whether or not to colocate the Jujutsu repo with the git repo - #[arg(long)] - colocate: bool, -} - -/// Push to a Git remote -/// -/// By default, pushes any branches pointing to -/// `remote_branches(remote=)..@`. Use `--branch` to push specific -/// branches. Use `--all` to push all branches. Use `--change` to generate -/// branch names based on the change IDs of specific commits. -/// -/// Before the command actually moves, creates, or deletes a remote branch, it -/// makes several [safety checks]. If there is a problem, you may need to run -/// `jj git fetch --remote ` and/or resolve some [branch -/// conflicts]. -/// -/// [safety checks]: -/// https://martinvonz.github.io/jj/latest/branches/#pushing-branches-safety-checks -/// -/// [branch conflicts]: -/// https://martinvonz.github.io/jj/latest/branches/#conflicts - -#[derive(clap::Args, Clone, Debug)] -#[command(group(ArgGroup::new("specific").args(&["branch", "change", "revisions"]).multiple(true)))] -#[command(group(ArgGroup::new("what").args(&["all", "deleted", "tracked"]).conflicts_with("specific")))] -pub struct GitPushArgs { - /// The remote to push to (only named remotes are supported) - #[arg(long)] - remote: Option, - /// Push only this branch, or branches matching a pattern (can be repeated) - /// - /// By default, the specified name matches exactly. Use `glob:` prefix to - /// select branches by wildcard pattern. For details, see - /// https://martinvonz.github.io/jj/latest/revsets#string-patterns. - #[arg(long, short, value_parser = StringPattern::parse)] - branch: Vec, - /// Push all branches (including deleted branches) - #[arg(long)] - all: bool, - /// Push all tracked branches (including deleted branches) - /// - /// This usually means that the branch was already pushed to or fetched from - /// the relevant remote. For details, see - /// https://martinvonz.github.io/jj/latest/branches#remotes-and-tracked-branches - #[arg(long)] - tracked: bool, - /// Push all deleted branches - /// - /// Only tracked branches can be successfully deleted on the remote. A - /// warning will be printed if any untracked branches on the remote - /// correspond to missing local branches. - #[arg(long)] - deleted: bool, - /// Allow pushing commits with empty descriptions - #[arg(long)] - allow_empty_description: bool, - /// Push branches pointing to these commits (can be repeated) - #[arg(long, short)] - revisions: Vec, - /// Push this commit by creating a branch based on its change ID (can be - /// repeated) - #[arg(long, short)] - change: Vec, - /// Only display what will change on the remote - #[arg(long)] - dry_run: bool, -} - -/// Update repo with changes made in the underlying Git repo -/// -/// If a working-copy commit gets abandoned, it will be given a new, empty -/// commit. This is true in general; it is not specific to this command. -#[derive(clap::Args, Clone, Debug)] -pub struct GitImportArgs {} - -/// Update the underlying Git repo with changes made in the repo -#[derive(clap::Args, Clone, Debug)] -pub struct GitExportArgs {} - -/// FOR INTERNAL USE ONLY Interact with git submodules -#[derive(Subcommand, Clone, Debug)] -pub enum GitSubmoduleCommand { - /// Print the relevant contents from .gitmodules. For debugging purposes - /// only. - PrintGitmodules(GitSubmodulePrintGitmodulesArgs), -} - -/// Print debugging info about Git submodules -#[derive(clap::Args, Clone, Debug)] -#[command(hide = true)] -pub struct GitSubmodulePrintGitmodulesArgs { - /// Read .gitmodules from the given revision. - #[arg(long, short = 'r', default_value = "@")] - revisions: RevisionArg, -} - -fn make_branch_term(branch_names: &[impl fmt::Display]) -> String { - match branch_names { - [branch_name] => format!("branch {}", branch_name), - branch_names => format!("branches {}", branch_names.iter().join(", ")), - } -} - -fn map_git_error(err: git2::Error) -> CommandError { - if err.class() == git2::ErrorClass::Ssh { - let hint = - if err.code() == git2::ErrorCode::Certificate && std::env::var_os("HOME").is_none() { - "The HOME environment variable is not set, and might be required for Git to \ - successfully load certificates. Try setting it to the path of a directory that \ - contains a `.ssh` directory." - } else { - "Jujutsu uses libssh2, which doesn't respect ~/.ssh/config. Does `ssh -F \ - /dev/null` to the host work?" - }; - - user_error_with_hint(err, hint) - } else { - user_error(err.to_string()) - } -} - -pub fn maybe_add_gitignore(workspace_command: &WorkspaceCommandHelper) -> Result<(), CommandError> { - if workspace_command.working_copy_shared_with_git() { - std::fs::write( - workspace_command - .workspace_root() - .join(".jj") - .join(".gitignore"), - "/*\n", - ) - .map_err(|e| user_error_with_message("Failed to write .jj/.gitignore file", e)) - } else { - Ok(()) - } -} - -fn cmd_git_remote_add( - ui: &mut Ui, - command: &CommandHelper, - args: &GitRemoteAddArgs, -) -> Result<(), CommandError> { - let workspace_command = command.workspace_helper(ui)?; - let repo = workspace_command.repo(); - let git_repo = get_git_repo(repo.store())?; - git::add_remote(&git_repo, &args.remote, &args.url)?; - Ok(()) -} - -fn cmd_git_remote_remove( - ui: &mut Ui, - command: &CommandHelper, - args: &GitRemoteRemoveArgs, -) -> Result<(), CommandError> { - let mut workspace_command = command.workspace_helper(ui)?; - let repo = workspace_command.repo(); - let git_repo = get_git_repo(repo.store())?; - let mut tx = workspace_command.start_transaction(); - git::remove_remote(tx.mut_repo(), &git_repo, &args.remote)?; - if tx.mut_repo().has_changes() { - tx.finish(ui, format!("remove git remote {}", &args.remote)) - } else { - Ok(()) // Do not print "Nothing changed." - } -} - -fn cmd_git_remote_rename( - ui: &mut Ui, - command: &CommandHelper, - args: &GitRemoteRenameArgs, -) -> Result<(), CommandError> { - let mut workspace_command = command.workspace_helper(ui)?; - let repo = workspace_command.repo(); - let git_repo = get_git_repo(repo.store())?; - let mut tx = workspace_command.start_transaction(); - git::rename_remote(tx.mut_repo(), &git_repo, &args.old, &args.new)?; - if tx.mut_repo().has_changes() { - tx.finish( - ui, - format!("rename git remote {} to {}", &args.old, &args.new), - ) - } else { - Ok(()) // Do not print "Nothing changed." - } -} - -fn cmd_git_remote_list( - ui: &mut Ui, - command: &CommandHelper, - _args: &GitRemoteListArgs, -) -> Result<(), CommandError> { - let workspace_command = command.workspace_helper(ui)?; - let repo = workspace_command.repo(); - let git_repo = get_git_repo(repo.store())?; - for remote_name in git_repo.remotes()?.iter().flatten() { - let remote = git_repo.find_remote(remote_name)?; - writeln!( - ui.stdout(), - "{} {}", - remote_name, - remote.url().unwrap_or("") - )?; - } - Ok(()) -} - -pub fn git_init( - ui: &mut Ui, - command: &CommandHelper, - workspace_root: &Path, - colocate: bool, - git_repo: Option<&str>, -) -> Result<(), CommandError> { - #[derive(Clone, Debug)] - enum GitInitMode { - Colocate, - External(PathBuf), - Internal, - } - - let colocated_git_repo_path = workspace_root.join(".git"); - let init_mode = if colocate { - if colocated_git_repo_path.exists() { - GitInitMode::External(colocated_git_repo_path) - } else { - GitInitMode::Colocate - } - } else if let Some(path_str) = git_repo { - let mut git_repo_path = command.cwd().join(path_str); - if !git_repo_path.ends_with(".git") { - git_repo_path.push(".git"); - // Undo if .git doesn't exist - likely a bare repo. - if !git_repo_path.exists() { - git_repo_path.pop(); - } - } - GitInitMode::External(git_repo_path) - } else { - if colocated_git_repo_path.exists() { - return Err(user_error_with_hint( - "Did not create a jj repo because there is an existing Git repo in this directory.", - "To create a repo backed by the existing Git repo, run `jj git init --colocate` \ - instead.", - )); - } - GitInitMode::Internal - }; - - match &init_mode { - GitInitMode::Colocate => { - let (workspace, repo) = - Workspace::init_colocated_git(command.settings(), workspace_root)?; - let workspace_command = command.for_loaded_repo(ui, workspace, repo)?; - maybe_add_gitignore(&workspace_command)?; - } - GitInitMode::External(git_repo_path) => { - let (workspace, repo) = - Workspace::init_external_git(command.settings(), workspace_root, git_repo_path)?; - // Import refs first so all the reachable commits are indexed in - // chronological order. - let colocated = is_colocated_git_workspace(&workspace, &repo); - let repo = init_git_refs(ui, command, repo, colocated)?; - let mut workspace_command = command.for_loaded_repo(ui, workspace, repo)?; - maybe_add_gitignore(&workspace_command)?; - workspace_command.maybe_snapshot(ui)?; - if !workspace_command.working_copy_shared_with_git() { - let mut tx = workspace_command.start_transaction(); - jj_lib::git::import_head(tx.mut_repo())?; - if let Some(git_head_id) = tx.mut_repo().view().git_head().as_normal().cloned() { - let git_head_commit = tx.mut_repo().store().get_commit(&git_head_id)?; - tx.check_out(&git_head_commit)?; - } - if tx.mut_repo().has_changes() { - tx.finish(ui, "import git head")?; - } - } - print_trackable_remote_branches(ui, workspace_command.repo().view())?; - } - GitInitMode::Internal => { - Workspace::init_internal_git(command.settings(), workspace_root)?; - } - } - Ok(()) -} - -/// Imports branches and tags from the underlying Git repo, exports changes if -/// the repo is colocated. -/// -/// This is similar to `WorkspaceCommandHelper::import_git_refs()`, but never -/// moves the Git HEAD to the working copy parent. -fn init_git_refs( - ui: &mut Ui, - command: &CommandHelper, - repo: Arc, - colocated: bool, -) -> Result, CommandError> { - let mut tx = start_repo_transaction(&repo, command.settings(), command.string_args()); - // There should be no old refs to abandon, but enforce it. - let mut git_settings = command.settings().git_settings(); - git_settings.abandon_unreachable_commits = false; - let stats = git::import_some_refs( - tx.mut_repo(), - &git_settings, - // Initial import shouldn't fail because of reserved remote name. - |ref_name| !git::is_reserved_git_remote_ref(ref_name), - )?; - if !tx.mut_repo().has_changes() { - return Ok(repo); - } - print_git_import_stats(ui, tx.repo(), &stats, false)?; - if colocated { - // If git.auto-local-branch = true, local branches could be created for - // the imported remote branches. - let failed_branches = git::export_refs(tx.mut_repo())?; - print_failed_git_export(ui, &failed_branches)?; - } - let repo = tx.commit("import git refs"); - writeln!( - ui.status(), - "Done importing changes from the underlying Git repo." - )?; - Ok(repo) -} - -fn cmd_git_init( - ui: &mut Ui, - command: &CommandHelper, - args: &GitInitArgs, -) -> Result<(), CommandError> { - let cwd = command.cwd(); - let wc_path = cwd.join(&args.destination); - let wc_path = file_util::create_or_reuse_dir(&wc_path) - .and_then(|_| wc_path.canonicalize()) - .map_err(|e| user_error_with_message("Failed to create workspace", e))?; - - git_init( - ui, - command, - &wc_path, - args.colocate, - args.git_repo.as_deref(), - )?; - - let relative_wc_path = file_util::relative_path(cwd, &wc_path); - writeln!( - ui.status(), - r#"Initialized repo in "{}""#, - relative_wc_path.display() - )?; - - Ok(()) -} - -#[tracing::instrument(skip(ui, command))] -fn cmd_git_fetch( - ui: &mut Ui, - command: &CommandHelper, - args: &GitFetchArgs, -) -> Result<(), CommandError> { - let mut workspace_command = command.workspace_helper(ui)?; - let git_repo = get_git_repo(workspace_command.repo().store())?; - let remotes = if args.all_remotes { - get_all_remotes(&git_repo)? - } else if args.remotes.is_empty() { - get_default_fetch_remotes(ui, command.settings(), &git_repo)? - } else { - args.remotes.clone() - }; - let mut tx = workspace_command.start_transaction(); - for remote in &remotes { - let stats = with_remote_git_callbacks(ui, None, |cb| { - git::fetch( - tx.mut_repo(), - &git_repo, - remote, - &args.branch, - cb, - &command.settings().git_settings(), - ) - }) - .map_err(|err| match err { - GitFetchError::InvalidBranchPattern => { - if args - .branch - .iter() - .any(|pattern| pattern.as_exact().map_or(false, |s| s.contains('*'))) - { - user_error_with_hint( - err, - "Prefix the pattern with `glob:` to expand `*` as a glob", - ) - } else { - user_error(err) - } - } - GitFetchError::GitImportError(err) => err.into(), - GitFetchError::InternalGitError(err) => map_git_error(err), - _ => user_error(err), - })?; - print_git_import_stats(ui, tx.repo(), &stats.import_stats, true)?; - } - tx.finish( - ui, - format!("fetch from git remote(s) {}", remotes.iter().join(",")), - )?; - Ok(()) -} - -fn get_single_remote(git_repo: &git2::Repository) -> Result, CommandError> { - let git_remotes = git_repo.remotes()?; - Ok(match git_remotes.len() { - 1 => git_remotes.get(0).map(ToOwned::to_owned), - _ => None, - }) -} - -const DEFAULT_REMOTE: &str = "origin"; - -fn get_default_fetch_remotes( - ui: &Ui, - settings: &UserSettings, - git_repo: &git2::Repository, -) -> Result, CommandError> { - const KEY: &str = "git.fetch"; - if let Ok(remotes) = settings.config().get(KEY) { - Ok(remotes) - } else if let Some(remote) = settings.config().get_string(KEY).optional()? { - Ok(vec![remote]) - } else if let Some(remote) = get_single_remote(git_repo)? { - // if nothing was explicitly configured, try to guess - if remote != DEFAULT_REMOTE { - if let Some(mut writer) = ui.hint_default() { - writeln!(writer, "Fetching from the only existing remote: {remote}")?; - } - } - Ok(vec![remote]) - } else { - Ok(vec![DEFAULT_REMOTE.to_owned()]) - } -} - -fn get_all_remotes(git_repo: &git2::Repository) -> Result, CommandError> { - let git_remotes = git_repo.remotes()?; - Ok(git_remotes - .iter() - .filter_map(|x| x.map(ToOwned::to_owned)) - .collect()) -} - -fn absolute_git_source(cwd: &Path, source: &str) -> String { - // Git appears to turn URL-like source to absolute path if local git directory - // exits, and fails because '$PWD/https' is unsupported protocol. Since it would - // be tedious to copy the exact git (or libgit2) behavior, we simply assume a - // source containing ':' is a URL, SSH remote, or absolute path with Windows - // drive letter. - if !source.contains(':') && Path::new(source).exists() { - // It's less likely that cwd isn't utf-8, so just fall back to original source. - cwd.join(source) - .into_os_string() - .into_string() - .unwrap_or_else(|_| source.to_owned()) - } else { - source.to_owned() - } -} - -fn clone_destination_for_source(source: &str) -> Option<&str> { - let destination = source.strip_suffix(".git").unwrap_or(source); - let destination = destination.strip_suffix('/').unwrap_or(destination); - destination - .rsplit_once(&['/', '\\', ':'][..]) - .map(|(_, name)| name) -} - -fn is_empty_dir(path: &Path) -> bool { - if let Ok(mut entries) = path.read_dir() { - entries.next().is_none() - } else { - false - } -} - -fn cmd_git_clone( - ui: &mut Ui, - command: &CommandHelper, - args: &GitCloneArgs, -) -> Result<(), CommandError> { - let remote_name = "origin"; - let source = absolute_git_source(command.cwd(), &args.source); - let wc_path_str = args - .destination - .as_deref() - .or_else(|| clone_destination_for_source(&source)) - .ok_or_else(|| user_error("No destination specified and wasn't able to guess it"))?; - let wc_path = command.cwd().join(wc_path_str); - let wc_path_existed = match fs::create_dir(&wc_path) { - Ok(()) => false, - Err(err) if err.kind() == io::ErrorKind::AlreadyExists => true, - Err(err) => { - return Err(user_error_with_message( - format!("Failed to create {wc_path_str}"), - err, - )); - } - }; - if wc_path_existed && !is_empty_dir(&wc_path) { - return Err(user_error( - "Destination path exists and is not an empty directory", - )); - } - - // Canonicalize because fs::remove_dir_all() doesn't seem to like e.g. - // `/some/path/.` - let canonical_wc_path: PathBuf = wc_path - .canonicalize() - .map_err(|err| user_error_with_message(format!("Failed to create {wc_path_str}"), err))?; - let clone_result = do_git_clone( - ui, - command, - args.colocate, - remote_name, - &source, - &canonical_wc_path, - ); - if clone_result.is_err() { - let clean_up_dirs = || -> io::Result<()> { - fs::remove_dir_all(canonical_wc_path.join(".jj"))?; - if args.colocate { - fs::remove_dir_all(canonical_wc_path.join(".git"))?; - } - if !wc_path_existed { - fs::remove_dir(&canonical_wc_path)?; - } - Ok(()) - }; - if let Err(err) = clean_up_dirs() { - writeln!( - ui.warning_default(), - "Failed to clean up {}: {}", - canonical_wc_path.display(), - err - ) - .ok(); - } - } - - let (mut workspace_command, stats) = clone_result?; - if let Some(default_branch) = &stats.default_branch { - let default_branch_remote_ref = workspace_command - .repo() - .view() - .get_remote_branch(default_branch, remote_name); - if let Some(commit_id) = default_branch_remote_ref.target.as_normal().cloned() { - let mut checkout_tx = workspace_command.start_transaction(); - // For convenience, create local branch as Git would do. - checkout_tx - .mut_repo() - .track_remote_branch(default_branch, remote_name); - if let Ok(commit) = checkout_tx.repo().store().get_commit(&commit_id) { - checkout_tx.check_out(&commit)?; - } - checkout_tx.finish(ui, "check out git remote's default branch")?; - } - } - Ok(()) -} - -fn do_git_clone( - ui: &mut Ui, - command: &CommandHelper, - colocate: bool, - remote_name: &str, - source: &str, - wc_path: &Path, -) -> Result<(WorkspaceCommandHelper, GitFetchStats), CommandError> { - let (workspace, repo) = if colocate { - Workspace::init_colocated_git(command.settings(), wc_path)? - } else { - Workspace::init_internal_git(command.settings(), wc_path)? - }; - let git_repo = get_git_repo(repo.store())?; - writeln!( - ui.status(), - r#"Fetching into new repo in "{}""#, - wc_path.display() - )?; - let mut workspace_command = command.for_loaded_repo(ui, workspace, repo)?; - maybe_add_gitignore(&workspace_command)?; - git_repo.remote(remote_name, source).unwrap(); - let mut fetch_tx = workspace_command.start_transaction(); - - let stats = with_remote_git_callbacks(ui, None, |cb| { - git::fetch( - fetch_tx.mut_repo(), - &git_repo, - remote_name, - &[StringPattern::everything()], - cb, - &command.settings().git_settings(), - ) - }) - .map_err(|err| match err { - GitFetchError::NoSuchRemote(_) => { - panic!("shouldn't happen as we just created the git remote") - } - GitFetchError::GitImportError(err) => CommandError::from(err), - GitFetchError::InternalGitError(err) => map_git_error(err), - GitFetchError::InvalidBranchPattern => { - unreachable!("we didn't provide any globs") - } - })?; - print_git_import_stats(ui, fetch_tx.repo(), &stats.import_stats, true)?; - fetch_tx.finish(ui, "fetch from git remote into empty repo")?; - Ok((workspace_command, stats)) -} - -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -enum BranchMoveDirection { - Forward, - Backward, - Sideways, -} - -fn cmd_git_push( - ui: &mut Ui, - command: &CommandHelper, - args: &GitPushArgs, -) -> Result<(), CommandError> { - let mut workspace_command = command.workspace_helper(ui)?; - let git_repo = get_git_repo(workspace_command.repo().store())?; - - let remote = if let Some(name) = &args.remote { - name.clone() - } else { - get_default_push_remote(ui, command.settings(), &git_repo)? - }; - - let repo = workspace_command.repo().clone(); - let mut tx = workspace_command.start_transaction(); - let tx_description; - let mut branch_updates = vec![]; - if args.all { - for (branch_name, targets) in repo.view().local_remote_branches(&remote) { - match classify_branch_update(branch_name, &remote, targets) { - Ok(Some(update)) => branch_updates.push((branch_name.to_owned(), update)), - Ok(None) => {} - Err(reason) => reason.print(ui)?, - } - } - tx_description = format!("push all branches to git remote {remote}"); - } else if args.tracked { - for (branch_name, targets) in repo.view().local_remote_branches(&remote) { - if !targets.remote_ref.is_tracking() { - continue; - } - match classify_branch_update(branch_name, &remote, targets) { - Ok(Some(update)) => branch_updates.push((branch_name.to_owned(), update)), - Ok(None) => {} - Err(reason) => reason.print(ui)?, - } - } - tx_description = format!("push all tracked branches to git remote {remote}"); - } else if args.deleted { - for (branch_name, targets) in repo.view().local_remote_branches(&remote) { - if targets.local_target.is_present() { - continue; - } - match classify_branch_update(branch_name, &remote, targets) { - Ok(Some(update)) => branch_updates.push((branch_name.to_owned(), update)), - Ok(None) => {} - Err(reason) => reason.print(ui)?, - } - } - tx_description = format!("push all deleted branches to git remote {remote}"); - } else { - let mut seen_branches: HashSet<&str> = HashSet::new(); - - // Process --change branches first because matching branches can be moved. - let change_branch_names = update_change_branches( - ui, - &mut tx, - &args.change, - &command.settings().push_branch_prefix(), - )?; - let change_branches = change_branch_names.iter().map(|branch_name| { - let targets = LocalAndRemoteRef { - local_target: tx.repo().view().get_local_branch(branch_name), - remote_ref: tx.repo().view().get_remote_branch(branch_name, &remote), - }; - (branch_name.as_ref(), targets) - }); - let branches_by_name = find_branches_to_push(repo.view(), &args.branch, &remote)?; - for (branch_name, targets) in change_branches.chain(branches_by_name.iter().copied()) { - if !seen_branches.insert(branch_name) { - continue; - } - match classify_branch_update(branch_name, &remote, targets) { - Ok(Some(update)) => branch_updates.push((branch_name.to_owned(), update)), - Ok(None) => writeln!( - ui.status(), - "Branch {branch_name}@{remote} already matches {branch_name}", - )?, - Err(reason) => return Err(reason.into()), - } - } - - let use_default_revset = - args.branch.is_empty() && args.change.is_empty() && args.revisions.is_empty(); - let branches_targeted = find_branches_targeted_by_revisions( - ui, - tx.base_workspace_helper(), - &remote, - &args.revisions, - use_default_revset, - )?; - for &(branch_name, targets) in &branches_targeted { - if !seen_branches.insert(branch_name) { - continue; - } - match classify_branch_update(branch_name, &remote, targets) { - Ok(Some(update)) => branch_updates.push((branch_name.to_owned(), update)), - Ok(None) => {} - Err(reason) => reason.print(ui)?, - } - } - - tx_description = format!( - "push {} to git remote {}", - make_branch_term( - &branch_updates - .iter() - .map(|(branch, _)| branch.as_str()) - .collect_vec() - ), - &remote - ); - } - if branch_updates.is_empty() { - writeln!(ui.status(), "Nothing changed.")?; - return Ok(()); - } - - let mut branch_push_direction = HashMap::new(); - for (branch_name, update) in &branch_updates { - let BranchPushUpdate { - old_target: Some(old_target), - new_target: Some(new_target), - } = update - else { - continue; - }; - assert_ne!(old_target, new_target); - branch_push_direction.insert( - branch_name.to_string(), - if repo.index().is_ancestor(old_target, new_target) { - BranchMoveDirection::Forward - } else if repo.index().is_ancestor(new_target, old_target) { - BranchMoveDirection::Backward - } else { - BranchMoveDirection::Sideways - }, - ); - } - - // Check if there are conflicts in any commits we're about to push that haven't - // already been pushed. - let new_heads = branch_updates - .iter() - .filter_map(|(_, update)| update.new_target.clone()) - .collect_vec(); - let mut old_heads = repo - .view() - .remote_branches(&remote) - .flat_map(|(_, old_head)| old_head.target.added_ids()) - .cloned() - .collect_vec(); - if old_heads.is_empty() { - old_heads.push(repo.store().root_commit_id().clone()); - } - for commit in revset::walk_revs(repo.as_ref(), &new_heads, &old_heads)? - .iter() - .commits(repo.store()) - { - let commit = commit?; - let mut reasons = vec![]; - if commit.description().is_empty() && !args.allow_empty_description { - reasons.push("it has no description"); - } - if commit.author().name.is_empty() - || commit.author().name == UserSettings::USER_NAME_PLACEHOLDER - || commit.author().email.is_empty() - || commit.author().email == UserSettings::USER_EMAIL_PLACEHOLDER - || commit.committer().name.is_empty() - || commit.committer().name == UserSettings::USER_NAME_PLACEHOLDER - || commit.committer().email.is_empty() - || commit.committer().email == UserSettings::USER_EMAIL_PLACEHOLDER - { - reasons.push("it has no author and/or committer set"); - } - if commit.has_conflict()? { - reasons.push("it has conflicts"); - } - if !reasons.is_empty() { - return Err(user_error(format!( - "Won't push commit {} since {}", - short_commit_hash(commit.id()), - reasons.join(" and ") - ))); - } - } - - writeln!(ui.status(), "Branch changes to push to {}:", &remote)?; - for (branch_name, update) in &branch_updates { - match (&update.old_target, &update.new_target) { - (Some(old_target), Some(new_target)) => { - let old = short_commit_hash(old_target); - let new = short_commit_hash(new_target); - // TODO(ilyagr): Add color. Once there is color, "Move branch ... sideways" may - // read more naturally than "Move sideways branch ...". Without color, it's hard - // to see at a glance if one branch among many was moved sideways (say). - // TODO: People on Discord suggest "Move branch ... forward by n commits", - // possibly "Move branch ... sideways (X forward, Y back)". - let msg = match branch_push_direction.get(branch_name).unwrap() { - BranchMoveDirection::Forward => { - format!("Move forward branch {branch_name} from {old} to {new}") - } - BranchMoveDirection::Backward => { - format!("Move backward branch {branch_name} from {old} to {new}") - } - BranchMoveDirection::Sideways => { - format!("Move sideways branch {branch_name} from {old} to {new}") - } - }; - writeln!(ui.status(), " {msg}")?; - } - (Some(old_target), None) => { - writeln!( - ui.status(), - " Delete branch {branch_name} from {}", - short_commit_hash(old_target) - )?; - } - (None, Some(new_target)) => { - writeln!( - ui.status(), - " Add branch {branch_name} to {}", - short_commit_hash(new_target) - )?; - } - (None, None) => { - panic!("Not pushing any change to branch {branch_name}"); - } - } - } - - if args.dry_run { - writeln!(ui.status(), "Dry-run requested, not pushing.")?; - return Ok(()); - } - - let targets = GitBranchPushTargets { branch_updates }; - let mut writer = GitSidebandProgressMessageWriter::new(ui); - let mut sideband_progress_callback = |progress_message: &[u8]| { - _ = writer.write(ui, progress_message); - }; - with_remote_git_callbacks(ui, Some(&mut sideband_progress_callback), |cb| { - git::push_branches(tx.mut_repo(), &git_repo, &remote, &targets, cb) - }) - .map_err(|err| match err { - GitPushError::InternalGitError(err) => map_git_error(err), - GitPushError::RefInUnexpectedLocation(refs) => user_error_with_hint( - format!( - "Refusing to push a branch that unexpectedly moved on the remote. Affected refs: \ - {}", - refs.join(", ") - ), - "Try fetching from the remote, then make the branch point to where you want it to be, \ - and push again.", - ), - _ => user_error(err), - })?; - writer.flush(ui)?; - tx.finish(ui, tx_description)?; - Ok(()) -} - -fn get_default_push_remote( - ui: &Ui, - settings: &UserSettings, - git_repo: &git2::Repository, -) -> Result { - if let Some(remote) = settings.config().get_string("git.push").optional()? { - Ok(remote) - } else if let Some(remote) = get_single_remote(git_repo)? { - // similar to get_default_fetch_remotes - if remote != DEFAULT_REMOTE { - if let Some(mut writer) = ui.hint_default() { - writeln!(writer, "Pushing to the only existing remote: {remote}")?; - } - } - Ok(remote) - } else { - Ok(DEFAULT_REMOTE.to_owned()) - } -} - -#[derive(Clone, Debug)] -struct RejectedBranchUpdateReason { - message: String, - hint: Option, -} - -impl RejectedBranchUpdateReason { - fn print(&self, ui: &Ui) -> io::Result<()> { - writeln!(ui.warning_default(), "{}", self.message)?; - if let Some(hint) = &self.hint { - if let Some(mut writer) = ui.hint_default() { - writeln!(writer, "{hint}")?; - } - } - Ok(()) - } -} - -impl From for CommandError { - fn from(reason: RejectedBranchUpdateReason) -> Self { - let RejectedBranchUpdateReason { message, hint } = reason; - let mut cmd_err = user_error(message); - cmd_err.extend_hints(hint); - cmd_err - } -} - -fn classify_branch_update( - branch_name: &str, - remote_name: &str, - targets: LocalAndRemoteRef, -) -> Result, RejectedBranchUpdateReason> { - let push_action = classify_branch_push_action(targets); - match push_action { - BranchPushAction::AlreadyMatches => Ok(None), - BranchPushAction::LocalConflicted => Err(RejectedBranchUpdateReason { - message: format!("Branch {branch_name} is conflicted"), - hint: Some( - "Run `jj branch list` to inspect, and use `jj branch set` to fix it up.".to_owned(), - ), - }), - BranchPushAction::RemoteConflicted => Err(RejectedBranchUpdateReason { - message: format!("Branch {branch_name}@{remote_name} is conflicted"), - hint: Some("Run `jj git fetch` to update the conflicted remote branch.".to_owned()), - }), - BranchPushAction::RemoteUntracked => Err(RejectedBranchUpdateReason { - message: format!("Non-tracking remote branch {branch_name}@{remote_name} exists"), - hint: Some(format!( - "Run `jj branch track {branch_name}@{remote_name}` to import the remote branch." - )), - }), - BranchPushAction::Update(update) => Ok(Some(update)), - } -} - -/// Creates or moves branches based on the change IDs. -fn update_change_branches( - ui: &Ui, - tx: &mut WorkspaceCommandTransaction, - changes: &[RevisionArg], - branch_prefix: &str, -) -> Result, CommandError> { - let mut branch_names = Vec::new(); - for change_arg in changes { - let workspace_command = tx.base_workspace_helper(); - let commit = workspace_command.resolve_single_rev(change_arg)?; - let mut branch_name = format!("{branch_prefix}{}", commit.change_id().hex()); - let view = tx.base_repo().view(); - if view.get_local_branch(&branch_name).is_absent() { - // A local branch with the full change ID doesn't exist already, so use the - // short ID if it's not ambiguous (which it shouldn't be most of the time). - let short_change_id = short_change_hash(commit.change_id()); - if workspace_command - .resolve_single_rev(&RevisionArg::from(short_change_id.clone())) - .is_ok() - { - // Short change ID is not ambiguous, so update the branch name to use it. - branch_name = format!("{branch_prefix}{short_change_id}"); - }; - } - if view.get_local_branch(&branch_name).is_absent() { - writeln!( - ui.status(), - "Creating branch {branch_name} for revision {change_arg}", - )?; - } - tx.mut_repo() - .set_local_branch_target(&branch_name, RefTarget::normal(commit.id().clone())); - branch_names.push(branch_name); - } - Ok(branch_names) -} - -fn find_branches_to_push<'a>( - view: &'a View, - branch_patterns: &[StringPattern], - remote_name: &str, -) -> Result)>, CommandError> { - let mut matching_branches = vec![]; - let mut unmatched_patterns = vec![]; - for pattern in branch_patterns { - let mut matches = view - .local_remote_branches_matching(pattern, remote_name) - .filter(|(_, targets)| { - // If the remote exists but is not tracking, the absent local shouldn't - // be considered a deleted branch. - targets.local_target.is_present() || targets.remote_ref.is_tracking() - }) - .peekable(); - if matches.peek().is_none() { - unmatched_patterns.push(pattern); - } - matching_branches.extend(matches); - } - match &unmatched_patterns[..] { - [] => Ok(matching_branches), - [pattern] if pattern.is_exact() => Err(user_error(format!("No such branch: {pattern}"))), - patterns => Err(user_error(format!( - "No matching branches for patterns: {}", - patterns.iter().join(", ") - ))), - } -} - -fn find_branches_targeted_by_revisions<'a>( - ui: &Ui, - workspace_command: &'a WorkspaceCommandHelper, - remote_name: &str, - revisions: &[RevisionArg], - use_default_revset: bool, -) -> Result)>, CommandError> { - let mut revision_commit_ids = HashSet::new(); - if use_default_revset { - let Some(wc_commit_id) = workspace_command.get_wc_commit_id().cloned() else { - return Err(user_error("Nothing checked out in this workspace")); - }; - let current_branches_expression = RevsetExpression::remote_branches( - StringPattern::everything(), - StringPattern::Exact(remote_name.to_owned()), - ) - .range(&RevsetExpression::commit(wc_commit_id)) - .intersection(&RevsetExpression::branches(StringPattern::everything())); - let current_branches_revset = - current_branches_expression.evaluate_programmatic(workspace_command.repo().as_ref())?; - revision_commit_ids.extend(current_branches_revset.iter()); - if revision_commit_ids.is_empty() { - writeln!( - ui.warning_default(), - "No branches found in the default push revset: \ - remote_branches(remote={remote_name})..@" - )?; - } - } - for rev_arg in revisions { - let mut expression = workspace_command.parse_revset(rev_arg)?; - expression.intersect_with(&RevsetExpression::branches(StringPattern::everything())); - let mut commit_ids = expression.evaluate_to_commit_ids()?.peekable(); - if commit_ids.peek().is_none() { - writeln!( - ui.warning_default(), - "No branches point to the specified revisions: {rev_arg}" - )?; - } - revision_commit_ids.extend(commit_ids); - } - let branches_targeted = workspace_command - .repo() - .view() - .local_remote_branches(remote_name) - .filter(|(_, targets)| { - let mut local_ids = targets.local_target.added_ids(); - local_ids.any(|id| revision_commit_ids.contains(id)) - }) - .collect_vec(); - Ok(branches_targeted) -} - -fn cmd_git_import( - ui: &mut Ui, - command: &CommandHelper, - _args: &GitImportArgs, -) -> Result<(), CommandError> { - let mut workspace_command = command.workspace_helper(ui)?; - let mut tx = workspace_command.start_transaction(); - // In non-colocated repo, HEAD@git will never be moved internally by jj. - // That's why cmd_git_export() doesn't export the HEAD ref. - git::import_head(tx.mut_repo())?; - let stats = git::import_refs(tx.mut_repo(), &command.settings().git_settings())?; - print_git_import_stats(ui, tx.repo(), &stats, true)?; - tx.finish(ui, "import git refs")?; - Ok(()) -} - -fn cmd_git_export( - ui: &mut Ui, - command: &CommandHelper, - _args: &GitExportArgs, -) -> Result<(), CommandError> { - let mut workspace_command = command.workspace_helper(ui)?; - let mut tx = workspace_command.start_transaction(); - let failed_branches = git::export_refs(tx.mut_repo())?; - tx.finish(ui, "export git refs")?; - print_failed_git_export(ui, &failed_branches)?; - Ok(()) -} - -fn cmd_git_submodule_print_gitmodules( - ui: &mut Ui, - command: &CommandHelper, - args: &GitSubmodulePrintGitmodulesArgs, -) -> Result<(), CommandError> { - let workspace_command = command.workspace_helper(ui)?; - let repo = workspace_command.repo(); - let commit = workspace_command.resolve_single_rev(&args.revisions)?; - let tree = commit.tree()?; - let gitmodules_path = RepoPath::from_internal_string(".gitmodules"); - let mut gitmodules_file = match tree.path_value(gitmodules_path)?.into_resolved() { - Ok(None) => { - writeln!(ui.status(), "No submodules!")?; - return Ok(()); - } - Ok(Some(TreeValue::File { id, .. })) => repo.store().read_file(gitmodules_path, &id)?, - _ => { - return Err(user_error(".gitmodules is not a file.")); - } - }; - - let submodules = parse_gitmodules(&mut gitmodules_file)?; - for (name, submodule) in submodules { - writeln!( - ui.stdout(), - "name:{}\nurl:{}\npath:{}\n\n", - name, - submodule.url, - submodule.path - )?; - } - Ok(()) -} - -pub fn cmd_git( - ui: &mut Ui, - command: &CommandHelper, - subcommand: &GitCommand, -) -> Result<(), CommandError> { - match subcommand { - GitCommand::Init(args) => cmd_git_init(ui, command, args), - GitCommand::Fetch(args) => cmd_git_fetch(ui, command, args), - GitCommand::Clone(args) => cmd_git_clone(ui, command, args), - GitCommand::Remote(GitRemoteCommand::Add(args)) => cmd_git_remote_add(ui, command, args), - GitCommand::Remote(GitRemoteCommand::Remove(args)) => { - cmd_git_remote_remove(ui, command, args) - } - GitCommand::Remote(GitRemoteCommand::Rename(args)) => { - cmd_git_remote_rename(ui, command, args) - } - GitCommand::Remote(GitRemoteCommand::List(args)) => cmd_git_remote_list(ui, command, args), - GitCommand::Push(args) => cmd_git_push(ui, command, args), - GitCommand::Import(args) => cmd_git_import(ui, command, args), - GitCommand::Export(args) => cmd_git_export(ui, command, args), - GitCommand::Submodule(GitSubmoduleCommand::PrintGitmodules(args)) => { - cmd_git_submodule_print_gitmodules(ui, command, args) - } - } -} diff --git a/cli/src/commands/git/clone.rs b/cli/src/commands/git/clone.rs new file mode 100644 index 0000000000..edef604b5c --- /dev/null +++ b/cli/src/commands/git/clone.rs @@ -0,0 +1,211 @@ +// Copyright 2020-2023 The Jujutsu Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::io::Write; +use std::path::{Path, PathBuf}; +use std::{fs, io}; + +use jj_lib::git::{self, GitFetchError, GitFetchStats}; +use jj_lib::repo::Repo; +use jj_lib::str_util::StringPattern; +use jj_lib::workspace::Workspace; + +use crate::cli_util::{CommandHelper, WorkspaceCommandHelper}; +use crate::command_error::{user_error, user_error_with_message, CommandError}; +use crate::commands::git::{map_git_error, maybe_add_gitignore}; +use crate::git_util::{get_git_repo, print_git_import_stats, with_remote_git_callbacks}; +use crate::ui::Ui; + +/// Create a new repo backed by a clone of a Git repo +/// +/// The Git repo will be a bare git repo stored inside the `.jj/` directory. +#[derive(clap::Args, Clone, Debug)] +pub struct GitCloneArgs { + /// URL or path of the Git repo to clone + #[arg(value_hint = clap::ValueHint::DirPath)] + source: String, + /// The directory to write the Jujutsu repo to + #[arg(value_hint = clap::ValueHint::DirPath)] + destination: Option, + /// Whether or not to colocate the Jujutsu repo with the git repo + #[arg(long)] + colocate: bool, +} + +fn absolute_git_source(cwd: &Path, source: &str) -> String { + // Git appears to turn URL-like source to absolute path if local git directory + // exits, and fails because '$PWD/https' is unsupported protocol. Since it would + // be tedious to copy the exact git (or libgit2) behavior, we simply assume a + // source containing ':' is a URL, SSH remote, or absolute path with Windows + // drive letter. + if !source.contains(':') && Path::new(source).exists() { + // It's less likely that cwd isn't utf-8, so just fall back to original source. + cwd.join(source) + .into_os_string() + .into_string() + .unwrap_or_else(|_| source.to_owned()) + } else { + source.to_owned() + } +} + +fn clone_destination_for_source(source: &str) -> Option<&str> { + let destination = source.strip_suffix(".git").unwrap_or(source); + let destination = destination.strip_suffix('/').unwrap_or(destination); + destination + .rsplit_once(&['/', '\\', ':'][..]) + .map(|(_, name)| name) +} + +fn is_empty_dir(path: &Path) -> bool { + if let Ok(mut entries) = path.read_dir() { + entries.next().is_none() + } else { + false + } +} + +pub fn cmd_git_clone( + ui: &mut Ui, + command: &CommandHelper, + args: &GitCloneArgs, +) -> Result<(), CommandError> { + let remote_name = "origin"; + let source = absolute_git_source(command.cwd(), &args.source); + let wc_path_str = args + .destination + .as_deref() + .or_else(|| clone_destination_for_source(&source)) + .ok_or_else(|| user_error("No destination specified and wasn't able to guess it"))?; + let wc_path = command.cwd().join(wc_path_str); + let wc_path_existed = match fs::create_dir(&wc_path) { + Ok(()) => false, + Err(err) if err.kind() == io::ErrorKind::AlreadyExists => true, + Err(err) => { + return Err(user_error_with_message( + format!("Failed to create {wc_path_str}"), + err, + )); + } + }; + if wc_path_existed && !is_empty_dir(&wc_path) { + return Err(user_error( + "Destination path exists and is not an empty directory", + )); + } + + // Canonicalize because fs::remove_dir_all() doesn't seem to like e.g. + // `/some/path/.` + let canonical_wc_path: PathBuf = wc_path + .canonicalize() + .map_err(|err| user_error_with_message(format!("Failed to create {wc_path_str}"), err))?; + let clone_result = do_git_clone( + ui, + command, + args.colocate, + remote_name, + &source, + &canonical_wc_path, + ); + if clone_result.is_err() { + let clean_up_dirs = || -> io::Result<()> { + fs::remove_dir_all(canonical_wc_path.join(".jj"))?; + if args.colocate { + fs::remove_dir_all(canonical_wc_path.join(".git"))?; + } + if !wc_path_existed { + fs::remove_dir(&canonical_wc_path)?; + } + Ok(()) + }; + if let Err(err) = clean_up_dirs() { + writeln!( + ui.warning_default(), + "Failed to clean up {}: {}", + canonical_wc_path.display(), + err + ) + .ok(); + } + } + + let (mut workspace_command, stats) = clone_result?; + if let Some(default_branch) = &stats.default_branch { + let default_branch_remote_ref = workspace_command + .repo() + .view() + .get_remote_branch(default_branch, remote_name); + if let Some(commit_id) = default_branch_remote_ref.target.as_normal().cloned() { + let mut checkout_tx = workspace_command.start_transaction(); + // For convenience, create local branch as Git would do. + checkout_tx + .mut_repo() + .track_remote_branch(default_branch, remote_name); + if let Ok(commit) = checkout_tx.repo().store().get_commit(&commit_id) { + checkout_tx.check_out(&commit)?; + } + checkout_tx.finish(ui, "check out git remote's default branch")?; + } + } + Ok(()) +} + +fn do_git_clone( + ui: &mut Ui, + command: &CommandHelper, + colocate: bool, + remote_name: &str, + source: &str, + wc_path: &Path, +) -> Result<(WorkspaceCommandHelper, GitFetchStats), CommandError> { + let (workspace, repo) = if colocate { + Workspace::init_colocated_git(command.settings(), wc_path)? + } else { + Workspace::init_internal_git(command.settings(), wc_path)? + }; + let git_repo = get_git_repo(repo.store())?; + writeln!( + ui.status(), + r#"Fetching into new repo in "{}""#, + wc_path.display() + )?; + let mut workspace_command = command.for_loaded_repo(ui, workspace, repo)?; + maybe_add_gitignore(&workspace_command)?; + git_repo.remote(remote_name, source).unwrap(); + let mut fetch_tx = workspace_command.start_transaction(); + + let stats = with_remote_git_callbacks(ui, None, |cb| { + git::fetch( + fetch_tx.mut_repo(), + &git_repo, + remote_name, + &[StringPattern::everything()], + cb, + &command.settings().git_settings(), + ) + }) + .map_err(|err| match err { + GitFetchError::NoSuchRemote(_) => { + panic!("shouldn't happen as we just created the git remote") + } + GitFetchError::GitImportError(err) => CommandError::from(err), + GitFetchError::InternalGitError(err) => map_git_error(err), + GitFetchError::InvalidBranchPattern => { + unreachable!("we didn't provide any globs") + } + })?; + print_git_import_stats(ui, fetch_tx.repo(), &stats.import_stats, true)?; + fetch_tx.finish(ui, "fetch from git remote into empty repo")?; + Ok((workspace_command, stats)) +} diff --git a/cli/src/commands/git/export.rs b/cli/src/commands/git/export.rs new file mode 100644 index 0000000000..0fa67b69a7 --- /dev/null +++ b/cli/src/commands/git/export.rs @@ -0,0 +1,37 @@ +// Copyright 2020-2023 The Jujutsu Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use jj_lib::git; + +use crate::cli_util::CommandHelper; +use crate::command_error::CommandError; +use crate::git_util::print_failed_git_export; +use crate::ui::Ui; + +/// Update the underlying Git repo with changes made in the repo +#[derive(clap::Args, Clone, Debug)] +pub struct GitExportArgs {} + +pub fn cmd_git_export( + ui: &mut Ui, + command: &CommandHelper, + _args: &GitExportArgs, +) -> Result<(), CommandError> { + let mut workspace_command = command.workspace_helper(ui)?; + let mut tx = workspace_command.start_transaction(); + let failed_branches = git::export_refs(tx.mut_repo())?; + tx.finish(ui, "export git refs")?; + print_failed_git_export(ui, &failed_branches)?; + Ok(()) +} diff --git a/cli/src/commands/git/fetch.rs b/cli/src/commands/git/fetch.rs new file mode 100644 index 0000000000..88efbce307 --- /dev/null +++ b/cli/src/commands/git/fetch.rs @@ -0,0 +1,135 @@ +// Copyright 2020-2023 The Jujutsu Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use itertools::Itertools; +use jj_lib::git::{self, GitFetchError}; +use jj_lib::repo::Repo; +use jj_lib::settings::{ConfigResultExt as _, UserSettings}; +use jj_lib::str_util::StringPattern; + +use crate::cli_util::CommandHelper; +use crate::command_error::{user_error, user_error_with_hint, CommandError}; +use crate::commands::git::{get_single_remote, map_git_error}; +use crate::git_util::{get_git_repo, print_git_import_stats, with_remote_git_callbacks}; +use crate::ui::Ui; + +/// Fetch from a Git remote +/// +/// If a working-copy commit gets abandoned, it will be given a new, empty +/// commit. This is true in general; it is not specific to this command. +#[derive(clap::Args, Clone, Debug)] +pub struct GitFetchArgs { + /// Fetch only some of the branches + /// + /// By default, the specified name matches exactly. Use `glob:` prefix to + /// expand `*` as a glob. The other wildcard characters aren't supported. + #[arg(long, short, default_value = "glob:*", value_parser = StringPattern::parse)] + branch: Vec, + /// The remote to fetch from (only named remotes are supported, can be + /// repeated) + #[arg(long = "remote", value_name = "remote")] + remotes: Vec, + /// Fetch from all remotes + #[arg(long, conflicts_with = "remotes")] + all_remotes: bool, +} + +#[tracing::instrument(skip(ui, command))] +pub fn cmd_git_fetch( + ui: &mut Ui, + command: &CommandHelper, + args: &GitFetchArgs, +) -> Result<(), CommandError> { + let mut workspace_command = command.workspace_helper(ui)?; + let git_repo = get_git_repo(workspace_command.repo().store())?; + let remotes = if args.all_remotes { + get_all_remotes(&git_repo)? + } else if args.remotes.is_empty() { + get_default_fetch_remotes(ui, command.settings(), &git_repo)? + } else { + args.remotes.clone() + }; + let mut tx = workspace_command.start_transaction(); + for remote in &remotes { + let stats = with_remote_git_callbacks(ui, None, |cb| { + git::fetch( + tx.mut_repo(), + &git_repo, + remote, + &args.branch, + cb, + &command.settings().git_settings(), + ) + }) + .map_err(|err| match err { + GitFetchError::InvalidBranchPattern => { + if args + .branch + .iter() + .any(|pattern| pattern.as_exact().map_or(false, |s| s.contains('*'))) + { + user_error_with_hint( + err, + "Prefix the pattern with `glob:` to expand `*` as a glob", + ) + } else { + user_error(err) + } + } + GitFetchError::GitImportError(err) => err.into(), + GitFetchError::InternalGitError(err) => map_git_error(err), + _ => user_error(err), + })?; + print_git_import_stats(ui, tx.repo(), &stats.import_stats, true)?; + } + tx.finish( + ui, + format!("fetch from git remote(s) {}", remotes.iter().join(",")), + )?; + Ok(()) +} + +const DEFAULT_REMOTE: &str = "origin"; + +fn get_default_fetch_remotes( + ui: &Ui, + settings: &UserSettings, + git_repo: &git2::Repository, +) -> Result, CommandError> { + const KEY: &str = "git.fetch"; + if let Ok(remotes) = settings.config().get(KEY) { + Ok(remotes) + } else if let Some(remote) = settings.config().get_string(KEY).optional()? { + Ok(vec![remote]) + } else if let Some(remote) = get_single_remote(git_repo)? { + // if nothing was explicitly configured, try to guess + if remote != DEFAULT_REMOTE { + writeln!( + ui.hint_default(), + "Fetching from the only existing remote: {remote}" + )?; + } + Ok(vec![remote]) + } else { + Ok(vec![DEFAULT_REMOTE.to_owned()]) + } +} + +fn get_all_remotes(git_repo: &git2::Repository) -> Result, CommandError> { + let git_remotes = git_repo.remotes()?; + Ok(git_remotes + .iter() + .filter_map(|x| x.map(ToOwned::to_owned)) + .collect()) +} diff --git a/cli/src/commands/git/import.rs b/cli/src/commands/git/import.rs new file mode 100644 index 0000000000..a10aa6b1e0 --- /dev/null +++ b/cli/src/commands/git/import.rs @@ -0,0 +1,43 @@ +// Copyright 2020-2023 The Jujutsu Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use jj_lib::git; + +use crate::cli_util::CommandHelper; +use crate::command_error::CommandError; +use crate::git_util::print_git_import_stats; +use crate::ui::Ui; + +/// Update repo with changes made in the underlying Git repo +/// +/// If a working-copy commit gets abandoned, it will be given a new, empty +/// commit. This is true in general; it is not specific to this command. +#[derive(clap::Args, Clone, Debug)] +pub struct GitImportArgs {} + +pub fn cmd_git_import( + ui: &mut Ui, + command: &CommandHelper, + _args: &GitImportArgs, +) -> Result<(), CommandError> { + let mut workspace_command = command.workspace_helper(ui)?; + let mut tx = workspace_command.start_transaction(); + // In non-colocated repo, HEAD@git will never be moved internally by jj. + // That's why cmd_git_export() doesn't export the HEAD ref. + git::import_head(tx.mut_repo())?; + let stats = git::import_refs(tx.mut_repo(), &command.settings().git_settings())?; + print_git_import_stats(ui, tx.repo(), &stats, true)?; + tx.finish(ui, "import git refs")?; + Ok(()) +} diff --git a/cli/src/commands/git/init.rs b/cli/src/commands/git/init.rs new file mode 100644 index 0000000000..2d0257e1b3 --- /dev/null +++ b/cli/src/commands/git/init.rs @@ -0,0 +1,212 @@ +// Copyright 2020-2023 The Jujutsu Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::io::Write; +use std::path::{Path, PathBuf}; +use std::sync::Arc; + +use jj_lib::repo::{ReadonlyRepo, Repo}; +use jj_lib::workspace::Workspace; +use jj_lib::{file_util, git}; + +use crate::cli_util::{print_trackable_remote_branches, start_repo_transaction, CommandHelper}; +use crate::command_error::{user_error_with_hint, user_error_with_message, CommandError}; +use crate::commands::git::maybe_add_gitignore; +use crate::git_util::{ + is_colocated_git_workspace, print_failed_git_export, print_git_import_stats, +}; +use crate::ui::Ui; + +/// Create a new Git backed repo. +#[derive(clap::Args, Clone, Debug)] +pub struct GitInitArgs { + /// The destination directory where the `jj` repo will be created. + /// If the directory does not exist, it will be created. + /// If no directory is given, the current directory is used. + /// + /// By default the `git` repo is under `$destination/.jj` + #[arg(default_value = ".", value_hint = clap::ValueHint::DirPath)] + destination: String, + + /// Specifies that the `jj` repo should also be a valid + /// `git` repo, allowing the use of both `jj` and `git` commands + /// in the same directory. + /// + /// This is done by placing the backing git repo into a `.git` directory in + /// the root of the `jj` repo along with the `.jj` directory. If the `.git` + /// directory already exists, all the existing commits will be imported. + /// + /// This option is mutually exclusive with `--git-repo`. + #[arg(long, conflicts_with = "git_repo")] + colocate: bool, + + /// Specifies a path to an **existing** git repository to be + /// used as the backing git repo for the newly created `jj` repo. + /// + /// If the specified `--git-repo` path happens to be the same as + /// the `jj` repo path (both .jj and .git directories are in the + /// same working directory), then both `jj` and `git` commands + /// will work on the same repo. This is called a co-located repo. + /// + /// This option is mutually exclusive with `--colocate`. + #[arg(long, conflicts_with = "colocate", value_hint = clap::ValueHint::DirPath)] + git_repo: Option, +} + +pub fn cmd_git_init( + ui: &mut Ui, + command: &CommandHelper, + args: &GitInitArgs, +) -> Result<(), CommandError> { + let cwd = command.cwd(); + let wc_path = cwd.join(&args.destination); + let wc_path = file_util::create_or_reuse_dir(&wc_path) + .and_then(|_| wc_path.canonicalize()) + .map_err(|e| user_error_with_message("Failed to create workspace", e))?; + + do_init( + ui, + command, + &wc_path, + args.colocate, + args.git_repo.as_deref(), + )?; + + let relative_wc_path = file_util::relative_path(cwd, &wc_path); + writeln!( + ui.status(), + r#"Initialized repo in "{}""#, + relative_wc_path.display() + )?; + + Ok(()) +} + +pub fn do_init( + ui: &mut Ui, + command: &CommandHelper, + workspace_root: &Path, + colocate: bool, + git_repo: Option<&str>, +) -> Result<(), CommandError> { + #[derive(Clone, Debug)] + enum GitInitMode { + Colocate, + External(PathBuf), + Internal, + } + + let colocated_git_repo_path = workspace_root.join(".git"); + let init_mode = if colocate { + if colocated_git_repo_path.exists() { + GitInitMode::External(colocated_git_repo_path) + } else { + GitInitMode::Colocate + } + } else if let Some(path_str) = git_repo { + let mut git_repo_path = command.cwd().join(path_str); + if !git_repo_path.ends_with(".git") { + git_repo_path.push(".git"); + // Undo if .git doesn't exist - likely a bare repo. + if !git_repo_path.exists() { + git_repo_path.pop(); + } + } + GitInitMode::External(git_repo_path) + } else { + if colocated_git_repo_path.exists() { + return Err(user_error_with_hint( + "Did not create a jj repo because there is an existing Git repo in this directory.", + "To create a repo backed by the existing Git repo, run `jj git init --colocate` \ + instead.", + )); + } + GitInitMode::Internal + }; + + match &init_mode { + GitInitMode::Colocate => { + let (workspace, repo) = + Workspace::init_colocated_git(command.settings(), workspace_root)?; + let workspace_command = command.for_loaded_repo(ui, workspace, repo)?; + maybe_add_gitignore(&workspace_command)?; + } + GitInitMode::External(git_repo_path) => { + let (workspace, repo) = + Workspace::init_external_git(command.settings(), workspace_root, git_repo_path)?; + // Import refs first so all the reachable commits are indexed in + // chronological order. + let colocated = is_colocated_git_workspace(&workspace, &repo); + let repo = init_git_refs(ui, command, repo, colocated)?; + let mut workspace_command = command.for_loaded_repo(ui, workspace, repo)?; + maybe_add_gitignore(&workspace_command)?; + workspace_command.maybe_snapshot(ui)?; + if !workspace_command.working_copy_shared_with_git() { + let mut tx = workspace_command.start_transaction(); + jj_lib::git::import_head(tx.mut_repo())?; + if let Some(git_head_id) = tx.mut_repo().view().git_head().as_normal().cloned() { + let git_head_commit = tx.mut_repo().store().get_commit(&git_head_id)?; + tx.check_out(&git_head_commit)?; + } + if tx.mut_repo().has_changes() { + tx.finish(ui, "import git head")?; + } + } + print_trackable_remote_branches(ui, workspace_command.repo().view())?; + } + GitInitMode::Internal => { + Workspace::init_internal_git(command.settings(), workspace_root)?; + } + } + Ok(()) +} + +/// Imports branches and tags from the underlying Git repo, exports changes if +/// the repo is colocated. +/// +/// This is similar to `WorkspaceCommandHelper::import_git_refs()`, but never +/// moves the Git HEAD to the working copy parent. +fn init_git_refs( + ui: &mut Ui, + command: &CommandHelper, + repo: Arc, + colocated: bool, +) -> Result, CommandError> { + let mut tx = start_repo_transaction(&repo, command.settings(), command.string_args()); + // There should be no old refs to abandon, but enforce it. + let mut git_settings = command.settings().git_settings(); + git_settings.abandon_unreachable_commits = false; + let stats = git::import_some_refs( + tx.mut_repo(), + &git_settings, + // Initial import shouldn't fail because of reserved remote name. + |ref_name| !git::is_reserved_git_remote_ref(ref_name), + )?; + if !tx.mut_repo().has_changes() { + return Ok(repo); + } + print_git_import_stats(ui, tx.repo(), &stats, false)?; + if colocated { + // If git.auto-local-branch = true, local branches could be created for + // the imported remote branches. + let failed_branches = git::export_refs(tx.mut_repo())?; + print_failed_git_export(ui, &failed_branches)?; + } + let repo = tx.commit("import git refs"); + writeln!( + ui.status(), + "Done importing changes from the underlying Git repo." + )?; + Ok(repo) +} diff --git a/cli/src/commands/git/mod.rs b/cli/src/commands/git/mod.rs new file mode 100644 index 0000000000..4b4fdd09f4 --- /dev/null +++ b/cli/src/commands/git/mod.rs @@ -0,0 +1,114 @@ +// Copyright 2020-2023 The Jujutsu Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +pub mod clone; +pub mod export; +pub mod fetch; +pub mod import; +pub mod init; +pub mod push; +pub mod remote; +pub mod submodule; + +use clap::Subcommand; + +use self::clone::{cmd_git_clone, GitCloneArgs}; +use self::export::{cmd_git_export, GitExportArgs}; +use self::fetch::{cmd_git_fetch, GitFetchArgs}; +use self::import::{cmd_git_import, GitImportArgs}; +use self::init::{cmd_git_init, GitInitArgs}; +use self::push::{cmd_git_push, GitPushArgs}; +use self::remote::{cmd_git_remote, RemoteCommand}; +use self::submodule::{cmd_git_submodule, GitSubmoduleCommand}; +use crate::cli_util::{CommandHelper, WorkspaceCommandHelper}; +use crate::command_error::{ + user_error, user_error_with_hint, user_error_with_message, CommandError, +}; +use crate::ui::Ui; + +/// Commands for working with Git remotes and the underlying Git repo +/// +/// For a comparison with Git, including a table of commands, see +/// https://github.com/martinvonz/jj/blob/main/docs/git-comparison.md. +#[derive(Subcommand, Clone, Debug)] +pub enum GitCommand { + Clone(GitCloneArgs), + Export(GitExportArgs), + Fetch(GitFetchArgs), + Import(GitImportArgs), + Init(GitInitArgs), + Push(GitPushArgs), + #[command(subcommand)] + Remote(RemoteCommand), + #[command(subcommand, hide = true)] + Submodule(GitSubmoduleCommand), +} + +pub fn cmd_git( + ui: &mut Ui, + command: &CommandHelper, + subcommand: &GitCommand, +) -> Result<(), CommandError> { + match subcommand { + GitCommand::Clone(args) => cmd_git_clone(ui, command, args), + GitCommand::Export(args) => cmd_git_export(ui, command, args), + GitCommand::Fetch(args) => cmd_git_fetch(ui, command, args), + GitCommand::Import(args) => cmd_git_import(ui, command, args), + GitCommand::Init(args) => cmd_git_init(ui, command, args), + GitCommand::Push(args) => cmd_git_push(ui, command, args), + GitCommand::Remote(args) => cmd_git_remote(ui, command, args), + GitCommand::Submodule(args) => cmd_git_submodule(ui, command, args), + } +} + +fn map_git_error(err: git2::Error) -> CommandError { + if err.class() == git2::ErrorClass::Ssh { + let hint = + if err.code() == git2::ErrorCode::Certificate && std::env::var_os("HOME").is_none() { + "The HOME environment variable is not set, and might be required for Git to \ + successfully load certificates. Try setting it to the path of a directory that \ + contains a `.ssh` directory." + } else { + "Jujutsu uses libssh2, which doesn't respect ~/.ssh/config. Does `ssh -F \ + /dev/null` to the host work?" + }; + + user_error_with_hint(err, hint) + } else { + user_error(err.to_string()) + } +} + +pub fn maybe_add_gitignore(workspace_command: &WorkspaceCommandHelper) -> Result<(), CommandError> { + if workspace_command.working_copy_shared_with_git() { + std::fs::write( + workspace_command + .workspace_root() + .join(".jj") + .join(".gitignore"), + "/*\n", + ) + .map_err(|e| user_error_with_message("Failed to write .jj/.gitignore file", e)) + } else { + Ok(()) + } +} + +fn get_single_remote(git_repo: &git2::Repository) -> Result, CommandError> { + let git_remotes = git_repo.remotes()?; + Ok(match git_remotes.len() { + 1 => git_remotes.get(0).map(ToOwned::to_owned), + _ => None, + }) +} diff --git a/cli/src/commands/git/push.rs b/cli/src/commands/git/push.rs new file mode 100644 index 0000000000..24b87daa0b --- /dev/null +++ b/cli/src/commands/git/push.rs @@ -0,0 +1,592 @@ +// Copyright 2020-2023 The Jujutsu Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::collections::{HashMap, HashSet}; +use std::io::Write; +use std::{fmt, io}; + +use clap::ArgGroup; +use itertools::Itertools; +use jj_lib::git::{self, GitBranchPushTargets, GitPushError}; +use jj_lib::object_id::ObjectId; +use jj_lib::op_store::RefTarget; +use jj_lib::refs::{ + classify_branch_push_action, BranchPushAction, BranchPushUpdate, LocalAndRemoteRef, +}; +use jj_lib::repo::Repo; +use jj_lib::revset::RevsetExpression; +use jj_lib::settings::{ConfigResultExt as _, UserSettings}; +use jj_lib::str_util::StringPattern; +use jj_lib::view::View; + +use crate::cli_util::{ + short_change_hash, short_commit_hash, CommandHelper, RevisionArg, WorkspaceCommandHelper, + WorkspaceCommandTransaction, +}; +use crate::command_error::{user_error, user_error_with_hint, CommandError}; +use crate::commands::git::{get_single_remote, map_git_error}; +use crate::git_util::{get_git_repo, with_remote_git_callbacks, GitSidebandProgressMessageWriter}; +use crate::revset_util; +use crate::ui::Ui; + +/// Push to a Git remote +/// +/// By default, pushes any branches pointing to +/// `remote_branches(remote=)..@`. Use `--branch` to push specific +/// branches. Use `--all` to push all branches. Use `--change` to generate +/// branch names based on the change IDs of specific commits. +/// +/// Before the command actually moves, creates, or deletes a remote branch, it +/// makes several [safety checks]. If there is a problem, you may need to run +/// `jj git fetch --remote ` and/or resolve some [branch +/// conflicts]. +/// +/// [safety checks]: +/// https://martinvonz.github.io/jj/latest/branches/#pushing-branches-safety-checks +/// +/// [branch conflicts]: +/// https://martinvonz.github.io/jj/latest/branches/#conflicts + +#[derive(clap::Args, Clone, Debug)] +#[command(group(ArgGroup::new("specific").args(&["branch", "change", "revisions"]).multiple(true)))] +#[command(group(ArgGroup::new("what").args(&["all", "deleted", "tracked"]).conflicts_with("specific")))] +pub struct GitPushArgs { + /// The remote to push to (only named remotes are supported) + #[arg(long)] + remote: Option, + /// Push only this branch, or branches matching a pattern (can be repeated) + /// + /// By default, the specified name matches exactly. Use `glob:` prefix to + /// select branches by wildcard pattern. For details, see + /// https://martinvonz.github.io/jj/latest/revsets#string-patterns. + #[arg(long, short, value_parser = StringPattern::parse)] + branch: Vec, + /// Push all branches (including deleted branches) + #[arg(long)] + all: bool, + /// Push all tracked branches (including deleted branches) + /// + /// This usually means that the branch was already pushed to or fetched from + /// the relevant remote. For details, see + /// https://martinvonz.github.io/jj/latest/branches#remotes-and-tracked-branches + #[arg(long)] + tracked: bool, + /// Push all deleted branches + /// + /// Only tracked branches can be successfully deleted on the remote. A + /// warning will be printed if any untracked branches on the remote + /// correspond to missing local branches. + #[arg(long)] + deleted: bool, + /// Allow pushing commits with empty descriptions + #[arg(long)] + allow_empty_description: bool, + /// Push branches pointing to these commits (can be repeated) + #[arg(long, short)] + revisions: Vec, + /// Push this commit by creating a branch based on its change ID (can be + /// repeated) + #[arg(long, short)] + change: Vec, + /// Only display what will change on the remote + #[arg(long)] + dry_run: bool, +} + +fn make_branch_term(branch_names: &[impl fmt::Display]) -> String { + match branch_names { + [branch_name] => format!("branch {}", branch_name), + branch_names => format!("branches {}", branch_names.iter().join(", ")), + } +} + +const DEFAULT_REMOTE: &str = "origin"; + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +enum BranchMoveDirection { + Forward, + Backward, + Sideways, +} + +pub fn cmd_git_push( + ui: &mut Ui, + command: &CommandHelper, + args: &GitPushArgs, +) -> Result<(), CommandError> { + let mut workspace_command = command.workspace_helper(ui)?; + let git_repo = get_git_repo(workspace_command.repo().store())?; + + let remote = if let Some(name) = &args.remote { + name.clone() + } else { + get_default_push_remote(ui, command.settings(), &git_repo)? + }; + + let repo = workspace_command.repo().clone(); + let mut tx = workspace_command.start_transaction(); + let tx_description; + let mut branch_updates = vec![]; + if args.all { + for (branch_name, targets) in repo.view().local_remote_branches(&remote) { + match classify_branch_update(branch_name, &remote, targets) { + Ok(Some(update)) => branch_updates.push((branch_name.to_owned(), update)), + Ok(None) => {} + Err(reason) => reason.print(ui)?, + } + } + tx_description = format!("push all branches to git remote {remote}"); + } else if args.tracked { + for (branch_name, targets) in repo.view().local_remote_branches(&remote) { + if !targets.remote_ref.is_tracking() { + continue; + } + match classify_branch_update(branch_name, &remote, targets) { + Ok(Some(update)) => branch_updates.push((branch_name.to_owned(), update)), + Ok(None) => {} + Err(reason) => reason.print(ui)?, + } + } + tx_description = format!("push all tracked branches to git remote {remote}"); + } else if args.deleted { + for (branch_name, targets) in repo.view().local_remote_branches(&remote) { + if targets.local_target.is_present() { + continue; + } + match classify_branch_update(branch_name, &remote, targets) { + Ok(Some(update)) => branch_updates.push((branch_name.to_owned(), update)), + Ok(None) => {} + Err(reason) => reason.print(ui)?, + } + } + tx_description = format!("push all deleted branches to git remote {remote}"); + } else { + let mut seen_branches: HashSet<&str> = HashSet::new(); + + // Process --change branches first because matching branches can be moved. + let change_branch_names = update_change_branches( + ui, + &mut tx, + &args.change, + &command.settings().push_branch_prefix(), + )?; + let change_branches = change_branch_names.iter().map(|branch_name| { + let targets = LocalAndRemoteRef { + local_target: tx.repo().view().get_local_branch(branch_name), + remote_ref: tx.repo().view().get_remote_branch(branch_name, &remote), + }; + (branch_name.as_ref(), targets) + }); + let branches_by_name = find_branches_to_push(repo.view(), &args.branch, &remote)?; + for (branch_name, targets) in change_branches.chain(branches_by_name.iter().copied()) { + if !seen_branches.insert(branch_name) { + continue; + } + match classify_branch_update(branch_name, &remote, targets) { + Ok(Some(update)) => branch_updates.push((branch_name.to_owned(), update)), + Ok(None) => writeln!( + ui.status(), + "Branch {branch_name}@{remote} already matches {branch_name}", + )?, + Err(reason) => return Err(reason.into()), + } + } + + let use_default_revset = + args.branch.is_empty() && args.change.is_empty() && args.revisions.is_empty(); + let branches_targeted = find_branches_targeted_by_revisions( + ui, + tx.base_workspace_helper(), + &remote, + &args.revisions, + use_default_revset, + )?; + for &(branch_name, targets) in &branches_targeted { + if !seen_branches.insert(branch_name) { + continue; + } + match classify_branch_update(branch_name, &remote, targets) { + Ok(Some(update)) => branch_updates.push((branch_name.to_owned(), update)), + Ok(None) => {} + Err(reason) => reason.print(ui)?, + } + } + + tx_description = format!( + "push {} to git remote {}", + make_branch_term( + &branch_updates + .iter() + .map(|(branch, _)| branch.as_str()) + .collect_vec() + ), + &remote + ); + } + if branch_updates.is_empty() { + writeln!(ui.status(), "Nothing changed.")?; + return Ok(()); + } + + let mut branch_push_direction = HashMap::new(); + for (branch_name, update) in &branch_updates { + let BranchPushUpdate { + old_target: Some(old_target), + new_target: Some(new_target), + } = update + else { + continue; + }; + assert_ne!(old_target, new_target); + branch_push_direction.insert( + branch_name.to_string(), + if repo.index().is_ancestor(old_target, new_target) { + BranchMoveDirection::Forward + } else if repo.index().is_ancestor(new_target, old_target) { + BranchMoveDirection::Backward + } else { + BranchMoveDirection::Sideways + }, + ); + } + + // Check if there are conflicts in any commits we're about to push that haven't + // already been pushed. + let new_heads = branch_updates + .iter() + .filter_map(|(_, update)| update.new_target.clone()) + .collect_vec(); + let old_heads = repo + .view() + .remote_branches(&remote) + .flat_map(|(_, old_head)| old_head.target.added_ids()) + .cloned() + .collect_vec(); + // (old_heads | immutable_heads() | root())..new_heads + let commits_to_push = RevsetExpression::commits(old_heads) + .union(&revset_util::parse_immutable_heads_expression( + &tx.base_workspace_helper().revset_parse_context(), + )?) + .range(&RevsetExpression::commits(new_heads)); + for commit in tx + .base_workspace_helper() + .attach_revset_evaluator(commits_to_push)? + .evaluate_to_commits()? + { + let commit = commit?; + let mut reasons = vec![]; + if commit.description().is_empty() && !args.allow_empty_description { + reasons.push("it has no description"); + } + if commit.author().name.is_empty() + || commit.author().name == UserSettings::USER_NAME_PLACEHOLDER + || commit.author().email.is_empty() + || commit.author().email == UserSettings::USER_EMAIL_PLACEHOLDER + || commit.committer().name.is_empty() + || commit.committer().name == UserSettings::USER_NAME_PLACEHOLDER + || commit.committer().email.is_empty() + || commit.committer().email == UserSettings::USER_EMAIL_PLACEHOLDER + { + reasons.push("it has no author and/or committer set"); + } + if commit.has_conflict()? { + reasons.push("it has conflicts"); + } + if !reasons.is_empty() { + return Err(user_error(format!( + "Won't push commit {} since {}", + short_commit_hash(commit.id()), + reasons.join(" and ") + ))); + } + } + + writeln!(ui.status(), "Branch changes to push to {}:", &remote)?; + for (branch_name, update) in &branch_updates { + match (&update.old_target, &update.new_target) { + (Some(old_target), Some(new_target)) => { + let old = short_commit_hash(old_target); + let new = short_commit_hash(new_target); + // TODO(ilyagr): Add color. Once there is color, "Move branch ... sideways" may + // read more naturally than "Move sideways branch ...". Without color, it's hard + // to see at a glance if one branch among many was moved sideways (say). + // TODO: People on Discord suggest "Move branch ... forward by n commits", + // possibly "Move branch ... sideways (X forward, Y back)". + let msg = match branch_push_direction.get(branch_name).unwrap() { + BranchMoveDirection::Forward => { + format!("Move forward branch {branch_name} from {old} to {new}") + } + BranchMoveDirection::Backward => { + format!("Move backward branch {branch_name} from {old} to {new}") + } + BranchMoveDirection::Sideways => { + format!("Move sideways branch {branch_name} from {old} to {new}") + } + }; + writeln!(ui.status(), " {msg}")?; + } + (Some(old_target), None) => { + writeln!( + ui.status(), + " Delete branch {branch_name} from {}", + short_commit_hash(old_target) + )?; + } + (None, Some(new_target)) => { + writeln!( + ui.status(), + " Add branch {branch_name} to {}", + short_commit_hash(new_target) + )?; + } + (None, None) => { + panic!("Not pushing any change to branch {branch_name}"); + } + } + } + + if args.dry_run { + writeln!(ui.status(), "Dry-run requested, not pushing.")?; + return Ok(()); + } + + let targets = GitBranchPushTargets { branch_updates }; + let mut writer = GitSidebandProgressMessageWriter::new(ui); + let mut sideband_progress_callback = |progress_message: &[u8]| { + _ = writer.write(ui, progress_message); + }; + with_remote_git_callbacks(ui, Some(&mut sideband_progress_callback), |cb| { + git::push_branches(tx.mut_repo(), &git_repo, &remote, &targets, cb) + }) + .map_err(|err| match err { + GitPushError::InternalGitError(err) => map_git_error(err), + GitPushError::RefInUnexpectedLocation(refs) => user_error_with_hint( + format!( + "Refusing to push a branch that unexpectedly moved on the remote. Affected refs: \ + {}", + refs.join(", ") + ), + "Try fetching from the remote, then make the branch point to where you want it to be, \ + and push again.", + ), + _ => user_error(err), + })?; + writer.flush(ui)?; + tx.finish(ui, tx_description)?; + Ok(()) +} + +fn get_default_push_remote( + ui: &Ui, + settings: &UserSettings, + git_repo: &git2::Repository, +) -> Result { + if let Some(remote) = settings.config().get_string("git.push").optional()? { + Ok(remote) + } else if let Some(remote) = get_single_remote(git_repo)? { + // similar to get_default_fetch_remotes + if remote != DEFAULT_REMOTE { + writeln!( + ui.hint_default(), + "Pushing to the only existing remote: {remote}" + )?; + } + Ok(remote) + } else { + Ok(DEFAULT_REMOTE.to_owned()) + } +} + +#[derive(Clone, Debug)] +struct RejectedBranchUpdateReason { + message: String, + hint: Option, +} + +impl RejectedBranchUpdateReason { + fn print(&self, ui: &Ui) -> io::Result<()> { + writeln!(ui.warning_default(), "{}", self.message)?; + if let Some(hint) = &self.hint { + writeln!(ui.hint_default(), "{hint}")?; + } + Ok(()) + } +} + +impl From for CommandError { + fn from(reason: RejectedBranchUpdateReason) -> Self { + let RejectedBranchUpdateReason { message, hint } = reason; + let mut cmd_err = user_error(message); + cmd_err.extend_hints(hint); + cmd_err + } +} + +fn classify_branch_update( + branch_name: &str, + remote_name: &str, + targets: LocalAndRemoteRef, +) -> Result, RejectedBranchUpdateReason> { + let push_action = classify_branch_push_action(targets); + match push_action { + BranchPushAction::AlreadyMatches => Ok(None), + BranchPushAction::LocalConflicted => Err(RejectedBranchUpdateReason { + message: format!("Branch {branch_name} is conflicted"), + hint: Some( + "Run `jj branch list` to inspect, and use `jj branch set` to fix it up.".to_owned(), + ), + }), + BranchPushAction::RemoteConflicted => Err(RejectedBranchUpdateReason { + message: format!("Branch {branch_name}@{remote_name} is conflicted"), + hint: Some("Run `jj git fetch` to update the conflicted remote branch.".to_owned()), + }), + BranchPushAction::RemoteUntracked => Err(RejectedBranchUpdateReason { + message: format!("Non-tracking remote branch {branch_name}@{remote_name} exists"), + hint: Some(format!( + "Run `jj branch track {branch_name}@{remote_name}` to import the remote branch." + )), + }), + BranchPushAction::Update(update) => Ok(Some(update)), + } +} + +/// Creates or moves branches based on the change IDs. +fn update_change_branches( + ui: &Ui, + tx: &mut WorkspaceCommandTransaction, + changes: &[RevisionArg], + branch_prefix: &str, +) -> Result, CommandError> { + if changes.is_empty() { + // NOTE: we don't want resolve_some_revsets_default_single to fail if the + // changes argument wasn't provided, so handle that + return Ok(vec![]); + } + + let mut branch_names = Vec::new(); + let workspace_command = tx.base_workspace_helper(); + let all_commits = workspace_command.resolve_some_revsets_default_single(changes)?; + + for commit in all_commits { + let workspace_command = tx.base_workspace_helper(); + let short_change_id = short_change_hash(commit.change_id()); + let mut branch_name = format!("{branch_prefix}{}", commit.change_id().hex()); + let view = tx.base_repo().view(); + if view.get_local_branch(&branch_name).is_absent() { + // A local branch with the full change ID doesn't exist already, so use the + // short ID if it's not ambiguous (which it shouldn't be most of the time). + if workspace_command + .resolve_single_rev(&RevisionArg::from(short_change_id.clone())) + .is_ok() + { + // Short change ID is not ambiguous, so update the branch name to use it. + branch_name = format!("{branch_prefix}{short_change_id}"); + }; + } + if view.get_local_branch(&branch_name).is_absent() { + writeln!( + ui.status(), + "Creating branch {branch_name} for revision {short_change_id}", + )?; + } + tx.mut_repo() + .set_local_branch_target(&branch_name, RefTarget::normal(commit.id().clone())); + branch_names.push(branch_name); + } + Ok(branch_names) +} + +fn find_branches_to_push<'a>( + view: &'a View, + branch_patterns: &[StringPattern], + remote_name: &str, +) -> Result)>, CommandError> { + let mut matching_branches = vec![]; + let mut unmatched_patterns = vec![]; + for pattern in branch_patterns { + let mut matches = view + .local_remote_branches_matching(pattern, remote_name) + .filter(|(_, targets)| { + // If the remote exists but is not tracking, the absent local shouldn't + // be considered a deleted branch. + targets.local_target.is_present() || targets.remote_ref.is_tracking() + }) + .peekable(); + if matches.peek().is_none() { + unmatched_patterns.push(pattern); + } + matching_branches.extend(matches); + } + match &unmatched_patterns[..] { + [] => Ok(matching_branches), + [pattern] if pattern.is_exact() => Err(user_error(format!("No such branch: {pattern}"))), + patterns => Err(user_error(format!( + "No matching branches for patterns: {}", + patterns.iter().join(", ") + ))), + } +} + +fn find_branches_targeted_by_revisions<'a>( + ui: &Ui, + workspace_command: &'a WorkspaceCommandHelper, + remote_name: &str, + revisions: &[RevisionArg], + use_default_revset: bool, +) -> Result)>, CommandError> { + let mut revision_commit_ids = HashSet::new(); + if use_default_revset { + let Some(wc_commit_id) = workspace_command.get_wc_commit_id().cloned() else { + return Err(user_error("Nothing checked out in this workspace")); + }; + let current_branches_expression = RevsetExpression::remote_branches( + StringPattern::everything(), + StringPattern::Exact(remote_name.to_owned()), + ) + .range(&RevsetExpression::commit(wc_commit_id)) + .intersection(&RevsetExpression::branches(StringPattern::everything())); + let current_branches_revset = + current_branches_expression.evaluate_programmatic(workspace_command.repo().as_ref())?; + revision_commit_ids.extend(current_branches_revset.iter()); + if revision_commit_ids.is_empty() { + writeln!( + ui.warning_default(), + "No branches found in the default push revset: \ + remote_branches(remote={remote_name})..@" + )?; + } + } + for rev_arg in revisions { + let mut expression = workspace_command.parse_revset(rev_arg)?; + expression.intersect_with(&RevsetExpression::branches(StringPattern::everything())); + let mut commit_ids = expression.evaluate_to_commit_ids()?.peekable(); + if commit_ids.peek().is_none() { + writeln!( + ui.warning_default(), + "No branches point to the specified revisions: {rev_arg}" + )?; + } + revision_commit_ids.extend(commit_ids); + } + let branches_targeted = workspace_command + .repo() + .view() + .local_remote_branches(remote_name) + .filter(|(_, targets)| { + let mut local_ids = targets.local_target.added_ids(); + local_ids.any(|id| revision_commit_ids.contains(id)) + }) + .collect_vec(); + Ok(branches_targeted) +} diff --git a/cli/src/commands/git/remote/add.rs b/cli/src/commands/git/remote/add.rs new file mode 100644 index 0000000000..a66a7faac8 --- /dev/null +++ b/cli/src/commands/git/remote/add.rs @@ -0,0 +1,42 @@ +// Copyright 2020-2023 The Jujutsu Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use jj_lib::git; +use jj_lib::repo::Repo; + +use crate::cli_util::CommandHelper; +use crate::command_error::CommandError; +use crate::git_util::get_git_repo; +use crate::ui::Ui; + +/// Add a Git remote +#[derive(clap::Args, Clone, Debug)] +pub struct GitRemoteAddArgs { + /// The remote's name + remote: String, + /// The remote's URL + url: String, +} + +pub fn cmd_git_remote_add( + ui: &mut Ui, + command: &CommandHelper, + args: &GitRemoteAddArgs, +) -> Result<(), CommandError> { + let workspace_command = command.workspace_helper(ui)?; + let repo = workspace_command.repo(); + let git_repo = get_git_repo(repo.store())?; + git::add_remote(&git_repo, &args.remote, &args.url)?; + Ok(()) +} diff --git a/cli/src/commands/git/remote/list.rs b/cli/src/commands/git/remote/list.rs new file mode 100644 index 0000000000..fee2d32f47 --- /dev/null +++ b/cli/src/commands/git/remote/list.rs @@ -0,0 +1,46 @@ +// Copyright 2020-2023 The Jujutsu Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::io::Write; + +use jj_lib::repo::Repo; + +use crate::cli_util::CommandHelper; +use crate::command_error::CommandError; +use crate::git_util::get_git_repo; +use crate::ui::Ui; + +/// List Git remotes +#[derive(clap::Args, Clone, Debug)] +pub struct GitRemoteListArgs {} + +pub fn cmd_git_remote_list( + ui: &mut Ui, + command: &CommandHelper, + _args: &GitRemoteListArgs, +) -> Result<(), CommandError> { + let workspace_command = command.workspace_helper(ui)?; + let repo = workspace_command.repo(); + let git_repo = get_git_repo(repo.store())?; + for remote_name in git_repo.remotes()?.iter().flatten() { + let remote = git_repo.find_remote(remote_name)?; + writeln!( + ui.stdout(), + "{} {}", + remote_name, + remote.url().unwrap_or("") + )?; + } + Ok(()) +} diff --git a/cli/src/commands/git/remote/mod.rs b/cli/src/commands/git/remote/mod.rs new file mode 100644 index 0000000000..659fd82f6e --- /dev/null +++ b/cli/src/commands/git/remote/mod.rs @@ -0,0 +1,56 @@ +// Copyright 2020-2023 The Jujutsu Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +pub mod add; +pub mod list; +pub mod remove; +pub mod rename; +pub mod set_url; + +use clap::Subcommand; + +use self::add::{cmd_git_remote_add, GitRemoteAddArgs}; +use self::list::{cmd_git_remote_list, GitRemoteListArgs}; +use self::remove::{cmd_git_remote_remove, GitRemoteRemoveArgs}; +use self::rename::{cmd_git_remote_rename, GitRemoteRenameArgs}; +use self::set_url::{cmd_git_remote_set_url, GitRemoteSetUrlArgs}; +use crate::cli_util::CommandHelper; +use crate::command_error::CommandError; +use crate::ui::Ui; + +/// Manage Git remotes +/// +/// The Git repo will be a bare git repo stored inside the `.jj/` directory. +#[derive(Subcommand, Clone, Debug)] +pub enum RemoteCommand { + Add(GitRemoteAddArgs), + List(GitRemoteListArgs), + Remove(GitRemoteRemoveArgs), + Rename(GitRemoteRenameArgs), + SetUrl(GitRemoteSetUrlArgs), +} + +pub fn cmd_git_remote( + ui: &mut Ui, + command: &CommandHelper, + subcommand: &RemoteCommand, +) -> Result<(), CommandError> { + match subcommand { + RemoteCommand::Add(args) => cmd_git_remote_add(ui, command, args), + RemoteCommand::List(args) => cmd_git_remote_list(ui, command, args), + RemoteCommand::Remove(args) => cmd_git_remote_remove(ui, command, args), + RemoteCommand::Rename(args) => cmd_git_remote_rename(ui, command, args), + RemoteCommand::SetUrl(args) => cmd_git_remote_set_url(ui, command, args), + } +} diff --git a/cli/src/commands/git/remote/remove.rs b/cli/src/commands/git/remote/remove.rs new file mode 100644 index 0000000000..7a0125d7a9 --- /dev/null +++ b/cli/src/commands/git/remote/remove.rs @@ -0,0 +1,45 @@ +// Copyright 2020-2023 The Jujutsu Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use jj_lib::git; +use jj_lib::repo::Repo; + +use crate::cli_util::CommandHelper; +use crate::command_error::CommandError; +use crate::git_util::get_git_repo; +use crate::ui::Ui; + +/// Remove a Git remote and forget its branches +#[derive(clap::Args, Clone, Debug)] +pub struct GitRemoteRemoveArgs { + /// The remote's name + remote: String, +} + +pub fn cmd_git_remote_remove( + ui: &mut Ui, + command: &CommandHelper, + args: &GitRemoteRemoveArgs, +) -> Result<(), CommandError> { + let mut workspace_command = command.workspace_helper(ui)?; + let repo = workspace_command.repo(); + let git_repo = get_git_repo(repo.store())?; + let mut tx = workspace_command.start_transaction(); + git::remove_remote(tx.mut_repo(), &git_repo, &args.remote)?; + if tx.mut_repo().has_changes() { + tx.finish(ui, format!("remove git remote {}", &args.remote)) + } else { + Ok(()) // Do not print "Nothing changed." + } +} diff --git a/cli/src/commands/git/remote/rename.rs b/cli/src/commands/git/remote/rename.rs new file mode 100644 index 0000000000..b02534062a --- /dev/null +++ b/cli/src/commands/git/remote/rename.rs @@ -0,0 +1,50 @@ +// Copyright 2020-2023 The Jujutsu Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use jj_lib::git; +use jj_lib::repo::Repo; + +use crate::cli_util::CommandHelper; +use crate::command_error::CommandError; +use crate::git_util::get_git_repo; +use crate::ui::Ui; + +/// Rename a Git remote +#[derive(clap::Args, Clone, Debug)] +pub struct GitRemoteRenameArgs { + /// The name of an existing remote + old: String, + /// The desired name for `old` + new: String, +} + +pub fn cmd_git_remote_rename( + ui: &mut Ui, + command: &CommandHelper, + args: &GitRemoteRenameArgs, +) -> Result<(), CommandError> { + let mut workspace_command = command.workspace_helper(ui)?; + let repo = workspace_command.repo(); + let git_repo = get_git_repo(repo.store())?; + let mut tx = workspace_command.start_transaction(); + git::rename_remote(tx.mut_repo(), &git_repo, &args.old, &args.new)?; + if tx.mut_repo().has_changes() { + tx.finish( + ui, + format!("rename git remote {} to {}", &args.old, &args.new), + ) + } else { + Ok(()) // Do not print "Nothing changed." + } +} diff --git a/cli/src/commands/git/remote/set_url.rs b/cli/src/commands/git/remote/set_url.rs new file mode 100644 index 0000000000..cee2da0559 --- /dev/null +++ b/cli/src/commands/git/remote/set_url.rs @@ -0,0 +1,42 @@ +// Copyright 2024 The Jujutsu Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use jj_lib::git; +use jj_lib::repo::Repo; + +use crate::cli_util::CommandHelper; +use crate::command_error::CommandError; +use crate::git_util::get_git_repo; +use crate::ui::Ui; + +/// Set the URL of a Git remote +#[derive(clap::Args, Clone, Debug)] +pub struct GitRemoteSetUrlArgs { + /// The remote's name + remote: String, + /// The desired url for `remote` + url: String, +} + +pub fn cmd_git_remote_set_url( + ui: &mut Ui, + command: &CommandHelper, + args: &GitRemoteSetUrlArgs, +) -> Result<(), CommandError> { + let workspace_command = command.workspace_helper(ui)?; + let repo = workspace_command.repo(); + let git_repo = get_git_repo(repo.store())?; + git::set_remote_url(&git_repo, &args.remote, &args.url)?; + Ok(()) +} diff --git a/cli/src/commands/git/submodule.rs b/cli/src/commands/git/submodule.rs new file mode 100644 index 0000000000..e7185143c5 --- /dev/null +++ b/cli/src/commands/git/submodule.rs @@ -0,0 +1,89 @@ +// Copyright 2020-2023 The Jujutsu Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::io::Write; + +use clap::Subcommand; +use jj_lib::backend::TreeValue; +use jj_lib::git::parse_gitmodules; +use jj_lib::repo::Repo; +use jj_lib::repo_path::RepoPath; + +use crate::cli_util::{CommandHelper, RevisionArg}; +use crate::command_error::{user_error, CommandError}; +use crate::ui::Ui; + +/// FOR INTERNAL USE ONLY Interact with git submodules +#[derive(Subcommand, Clone, Debug)] +pub enum GitSubmoduleCommand { + /// Print the relevant contents from .gitmodules. For debugging purposes + /// only. + PrintGitmodules(PrintArgs), +} + +pub fn cmd_git_submodule( + ui: &mut Ui, + command: &CommandHelper, + subcommand: &GitSubmoduleCommand, +) -> Result<(), CommandError> { + match subcommand { + GitSubmoduleCommand::PrintGitmodules(args) => cmd_submodule_print(ui, command, args), + } +} + +// TODO: break everything below into a separate file as soon as there is more +// than one subcommand here. + +/// Print debugging info about Git submodules +#[derive(clap::Args, Clone, Debug)] +#[command(hide = true)] +pub struct PrintArgs { + /// Read .gitmodules from the given revision. + #[arg(long, short = 'r', default_value = "@")] + revisions: RevisionArg, +} + +fn cmd_submodule_print( + ui: &mut Ui, + command: &CommandHelper, + args: &PrintArgs, +) -> Result<(), CommandError> { + let workspace_command = command.workspace_helper(ui)?; + let repo = workspace_command.repo(); + let commit = workspace_command.resolve_single_rev(&args.revisions)?; + let tree = commit.tree()?; + let gitmodules_path = RepoPath::from_internal_string(".gitmodules"); + let mut gitmodules_file = match tree.path_value(gitmodules_path)?.into_resolved() { + Ok(None) => { + writeln!(ui.status(), "No submodules!")?; + return Ok(()); + } + Ok(Some(TreeValue::File { id, .. })) => repo.store().read_file(gitmodules_path, &id)?, + _ => { + return Err(user_error(".gitmodules is not a file.")); + } + }; + + let submodules = parse_gitmodules(&mut gitmodules_file)?; + for (name, submodule) in submodules { + writeln!( + ui.stdout(), + "name:{}\nurl:{}\npath:{}\n\n", + name, + submodule.url, + submodule.path + )?; + } + Ok(()) +} diff --git a/cli/src/commands/init.rs b/cli/src/commands/init.rs index 0e6d952dcb..1ec74ed479 100644 --- a/cli/src/commands/init.rs +++ b/cli/src/commands/init.rs @@ -60,12 +60,12 @@ pub(crate) fn cmd_init( // a colocated repo. let colocate = false; if args.git || args.git_repo.is_some() { - git::git_init(ui, command, &wc_path, colocate, args.git_repo.as_deref())?; + git::init::do_init(ui, command, &wc_path, colocate, args.git_repo.as_deref())?; writeln!( ui.warning_default(), "`--git` and `--git-repo` are deprecated. Use `jj git init` instead" - )?; + )? } else { if !command.settings().allow_native_backend() { return Err(user_error_with_hint( diff --git a/cli/src/commands/log.rs b/cli/src/commands/log.rs index d1afd286db..46fbcb1644 100644 --- a/cli/src/commands/log.rs +++ b/cli/src/commands/log.rs @@ -49,8 +49,16 @@ pub(crate) struct LogArgs { /// Limit number of revisions to show /// /// Applied after revisions are filtered and reordered. - #[arg(long, short)] + #[arg(long, short = 'n')] limit: Option, + // TODO: Delete `-l` alias in jj 0.25+ + #[arg( + short = 'l', + hide = true, + conflicts_with = "limit", + value_name = "LIMIT" + )] + deprecated_limit: Option, /// Don't show the graph, show a flat list of revisions #[arg(long)] no_graph: bool, @@ -137,6 +145,14 @@ pub(crate) fn cmd_log( let mut formatter = ui.stdout_formatter(); let formatter = formatter.as_mut(); + if args.deprecated_limit.is_some() { + writeln!( + ui.warning_default(), + "The -l shorthand is deprecated, use -n instead." + )?; + } + let limit = args.limit.or(args.deprecated_limit).unwrap_or(usize::MAX); + if !args.no_graph { let mut graph = get_graphlog(command.settings(), formatter.raw()); let forward_iter = TopoGroupedGraphIterator::new(revset.iter_graph()); @@ -145,7 +161,7 @@ pub(crate) fn cmd_log( } else { Box::new(forward_iter) }; - for (commit_id, edges) in iter.take(args.limit.unwrap_or(usize::MAX)) { + for (commit_id, edges) in iter.take(limit) { // The graph is keyed by (CommitId, is_synthetic) let mut graphlog_edges = vec![]; // TODO: Should we update revset.iter_graph() to yield this flag instead of all @@ -222,7 +238,7 @@ pub(crate) fn cmd_log( } else { Box::new(revset.iter()) }; - for commit_or_error in iter.commits(store).take(args.limit.unwrap_or(usize::MAX)) { + for commit_or_error in iter.commits(store).take(limit) { let commit = commit_or_error?; with_content_format .write(formatter, |formatter| template.format(&commit, formatter))?; diff --git a/cli/src/commands/mod.rs b/cli/src/commands/mod.rs index 188147ad59..4498ed398c 100644 --- a/cli/src/commands/mod.rs +++ b/cli/src/commands/mod.rs @@ -17,9 +17,7 @@ mod backout; #[cfg(feature = "bench")] mod bench; mod branch; -mod cat; mod checkout; -mod chmod; mod commit; mod config; mod debug; @@ -28,7 +26,7 @@ mod diff; mod diffedit; mod duplicate; mod edit; -mod files; +mod file; mod fix; mod git; mod init; @@ -77,11 +75,12 @@ enum Command { Bench(bench::BenchCommand), #[command(subcommand)] Branch(branch::BranchCommand), - #[command(alias = "print")] - Cat(cat::CatArgs), + #[command(alias = "print", hide = true)] + Cat(file::show::FileShowArgs), #[command(hide = true)] Checkout(checkout::CheckoutArgs), - Chmod(chmod::ChmodArgs), + #[command(hide = true)] + Chmod(file::chmod::FileChmodArgs), Commit(commit::CommitArgs), #[command(subcommand)] Config(config::ConfigCommand), @@ -92,7 +91,11 @@ enum Command { Diffedit(diffedit::DiffeditArgs), Duplicate(duplicate::DuplicateArgs), Edit(edit::EditArgs), - Files(files::FilesArgs), + #[command(subcommand)] + File(file::FileCommand), + /// List files in a revision (DEPRECATED use `jj file list`) + #[command(hide = true)] + Files(file::list::FileListArgs), Fix(fix::FixArgs), #[command(subcommand)] Git(git::GitCommand), @@ -134,7 +137,7 @@ enum Command { Run(run::RunArgs), Show(show::ShowArgs), #[command(subcommand)] - Sparse(sparse::SparseArgs), + Sparse(sparse::SparseCommand), Split(split::SplitArgs), Squash(squash::SquashArgs), Status(status::StatusArgs), @@ -143,7 +146,7 @@ enum Command { #[command(subcommand)] Util(util::UtilCommand), /// Undo an operation (shortcut for `jj op undo`) - Undo(operation::OperationUndoArgs), + Undo(operation::undo::OperationUndoArgs), Unsquash(unsquash::UnsquashArgs), Untrack(untrack::UntrackArgs), Version(version::VersionArgs), @@ -164,58 +167,57 @@ pub fn default_app() -> clap::Command { #[instrument(skip_all)] pub fn run_command(ui: &mut Ui, command_helper: &CommandHelper) -> Result<(), CommandError> { - let derived_subcommands: Command = Command::from_arg_matches(command_helper.matches()).unwrap(); - match &derived_subcommands { - Command::Version(sub_args) => version::cmd_version(ui, command_helper, sub_args), - Command::Init(sub_args) => init::cmd_init(ui, command_helper, sub_args), - Command::Config(sub_args) => config::cmd_config(ui, command_helper, sub_args), - Command::Checkout(sub_args) => checkout::cmd_checkout(ui, command_helper, sub_args), - Command::Untrack(sub_args) => untrack::cmd_untrack(ui, command_helper, sub_args), - Command::Files(sub_args) => files::cmd_files(ui, command_helper, sub_args), - Command::Cat(sub_args) => cat::cmd_cat(ui, command_helper, sub_args), - Command::Diff(sub_args) => diff::cmd_diff(ui, command_helper, sub_args), - Command::Show(sub_args) => show::cmd_show(ui, command_helper, sub_args), - Command::Status(sub_args) => status::cmd_status(ui, command_helper, sub_args), - Command::Log(sub_args) => log::cmd_log(ui, command_helper, sub_args), - Command::Interdiff(sub_args) => interdiff::cmd_interdiff(ui, command_helper, sub_args), - Command::Obslog(sub_args) => obslog::cmd_obslog(ui, command_helper, sub_args), - Command::Describe(sub_args) => describe::cmd_describe(ui, command_helper, sub_args), - Command::Commit(sub_args) => commit::cmd_commit(ui, command_helper, sub_args), - Command::Duplicate(sub_args) => duplicate::cmd_duplicate(ui, command_helper, sub_args), - Command::Abandon(sub_args) => abandon::cmd_abandon(ui, command_helper, sub_args), - Command::Edit(sub_args) => edit::cmd_edit(ui, command_helper, sub_args), - Command::Next(sub_args) => next::cmd_next(ui, command_helper, sub_args), - Command::Parallelize(sub_args) => { - parallelize::cmd_parallelize(ui, command_helper, sub_args) - } - Command::Prev(sub_args) => prev::cmd_prev(ui, command_helper, sub_args), - Command::New(sub_args) => new::cmd_new(ui, command_helper, sub_args), - Command::Move(sub_args) => r#move::cmd_move(ui, command_helper, sub_args), - Command::Squash(sub_args) => squash::cmd_squash(ui, command_helper, sub_args), - Command::Unsquash(sub_args) => unsquash::cmd_unsquash(ui, command_helper, sub_args), - Command::Restore(sub_args) => restore::cmd_restore(ui, command_helper, sub_args), - Command::Revert(_args) => revert(), - Command::Root(sub_args) => root::cmd_root(ui, command_helper, sub_args), - Command::Run(sub_args) => run::cmd_run(ui, command_helper, sub_args), - Command::Diffedit(sub_args) => diffedit::cmd_diffedit(ui, command_helper, sub_args), - Command::Split(sub_args) => split::cmd_split(ui, command_helper, sub_args), - Command::Merge(sub_args) => merge::cmd_merge(ui, command_helper, sub_args), - Command::Rebase(sub_args) => rebase::cmd_rebase(ui, command_helper, sub_args), - Command::Backout(sub_args) => backout::cmd_backout(ui, command_helper, sub_args), - Command::Fix(sub_args) => fix::cmd_fix(ui, command_helper, sub_args), - Command::Resolve(sub_args) => resolve::cmd_resolve(ui, command_helper, sub_args), - Command::Branch(sub_args) => branch::cmd_branch(ui, command_helper, sub_args), - Command::Undo(sub_args) => operation::cmd_op_undo(ui, command_helper, sub_args), - Command::Operation(sub_args) => operation::cmd_operation(ui, command_helper, sub_args), - Command::Workspace(sub_args) => workspace::cmd_workspace(ui, command_helper, sub_args), - Command::Sparse(sub_args) => sparse::cmd_sparse(ui, command_helper, sub_args), - Command::Tag(sub_args) => tag::cmd_tag(ui, command_helper, sub_args), - Command::Chmod(sub_args) => chmod::cmd_chmod(ui, command_helper, sub_args), - Command::Git(sub_args) => git::cmd_git(ui, command_helper, sub_args), - Command::Util(sub_args) => util::cmd_util(ui, command_helper, sub_args), + let subcommand = Command::from_arg_matches(command_helper.matches()).unwrap(); + match &subcommand { + Command::Abandon(args) => abandon::cmd_abandon(ui, command_helper, args), + Command::Backout(args) => backout::cmd_backout(ui, command_helper, args), #[cfg(feature = "bench")] - Command::Bench(sub_args) => bench::cmd_bench(ui, command_helper, sub_args), - Command::Debug(sub_args) => debug::cmd_debug(ui, command_helper, sub_args), + Command::Bench(args) => bench::cmd_bench(ui, command_helper, args), + Command::Branch(args) => branch::cmd_branch(ui, command_helper, args), + Command::Cat(args) => file::show::deprecated_cmd_cat(ui, command_helper, args), + Command::Checkout(args) => checkout::cmd_checkout(ui, command_helper, args), + Command::Chmod(args) => file::chmod::deprecated_cmd_chmod(ui, command_helper, args), + Command::Commit(args) => commit::cmd_commit(ui, command_helper, args), + Command::Config(args) => config::cmd_config(ui, command_helper, args), + Command::Debug(args) => debug::cmd_debug(ui, command_helper, args), + Command::Describe(args) => describe::cmd_describe(ui, command_helper, args), + Command::Diff(args) => diff::cmd_diff(ui, command_helper, args), + Command::Diffedit(args) => diffedit::cmd_diffedit(ui, command_helper, args), + Command::Duplicate(args) => duplicate::cmd_duplicate(ui, command_helper, args), + Command::Edit(args) => edit::cmd_edit(ui, command_helper, args), + Command::File(args) => file::cmd_file(ui, command_helper, args), + Command::Files(args) => file::list::deprecated_cmd_files(ui, command_helper, args), + Command::Fix(args) => fix::cmd_fix(ui, command_helper, args), + Command::Git(args) => git::cmd_git(ui, command_helper, args), + Command::Init(args) => init::cmd_init(ui, command_helper, args), + Command::Interdiff(args) => interdiff::cmd_interdiff(ui, command_helper, args), + Command::Log(args) => log::cmd_log(ui, command_helper, args), + Command::Merge(args) => merge::cmd_merge(ui, command_helper, args), + Command::Move(args) => r#move::cmd_move(ui, command_helper, args), + Command::New(args) => new::cmd_new(ui, command_helper, args), + Command::Next(args) => next::cmd_next(ui, command_helper, args), + Command::Obslog(args) => obslog::cmd_obslog(ui, command_helper, args), + Command::Operation(args) => operation::cmd_operation(ui, command_helper, args), + Command::Parallelize(args) => parallelize::cmd_parallelize(ui, command_helper, args), + Command::Prev(args) => prev::cmd_prev(ui, command_helper, args), + Command::Rebase(args) => rebase::cmd_rebase(ui, command_helper, args), + Command::Resolve(args) => resolve::cmd_resolve(ui, command_helper, args), + Command::Restore(args) => restore::cmd_restore(ui, command_helper, args), + Command::Revert(_args) => revert(), + Command::Root(args) => root::cmd_root(ui, command_helper, args), + Command::Run(args) => run::cmd_run(ui, command_helper, args), + Command::Show(args) => show::cmd_show(ui, command_helper, args), + Command::Sparse(args) => sparse::cmd_sparse(ui, command_helper, args), + Command::Split(args) => split::cmd_split(ui, command_helper, args), + Command::Squash(args) => squash::cmd_squash(ui, command_helper, args), + Command::Status(args) => status::cmd_status(ui, command_helper, args), + Command::Tag(args) => tag::cmd_tag(ui, command_helper, args), + Command::Undo(args) => operation::undo::cmd_op_undo(ui, command_helper, args), + Command::Unsquash(args) => unsquash::cmd_unsquash(ui, command_helper, args), + Command::Untrack(args) => untrack::cmd_untrack(ui, command_helper, args), + Command::Util(args) => util::cmd_util(ui, command_helper, args), + Command::Version(args) => version::cmd_version(ui, command_helper, args), + Command::Workspace(args) => workspace::cmd_workspace(ui, command_helper, args), } } diff --git a/cli/src/commands/next.rs b/cli/src/commands/next.rs index 8ae074fdb7..6a78a790b6 100644 --- a/cli/src/commands/next.rs +++ b/cli/src/commands/next.rs @@ -17,7 +17,7 @@ use std::io::Write; use itertools::Itertools; use jj_lib::commit::Commit; use jj_lib::repo::Repo; -use jj_lib::revset::{RevsetExpression, RevsetIteratorExt}; +use jj_lib::revset::{RevsetExpression, RevsetFilterPredicate, RevsetIteratorExt}; use crate::cli_util::{short_commit_hash, CommandHelper, WorkspaceCommandHelper}; use crate::command_error::{user_error, CommandError}; @@ -65,6 +65,9 @@ pub(crate) struct NextArgs { /// edit`). #[arg(long, short)] edit: bool, + /// Jump to the next conflicted descendant. + #[arg(long, conflicts_with = "offset")] + conflict: bool, } pub fn choose_commit<'a>( @@ -117,21 +120,29 @@ pub(crate) fn cmd_next( let wc_revset = RevsetExpression::commit(current_wc_id.clone()); // If we're editing, start at the working-copy commit. Otherwise, start from // its direct parent(s). - let target_revset = if edit { - wc_revset.descendants_at(args.offset) + let start_revset = if edit { + wc_revset.clone() } else { - wc_revset - .parents() - .descendants_at(args.offset) - // In previous versions we subtracted `wc_revset.descendants()`. That's - // unnecessary now that --edit is implied if `@` has descendants. - .minus(&wc_revset) + wc_revset.parents() }; + + let target_revset = if args.conflict { + start_revset + .children() + .descendants() + .filtered(RevsetFilterPredicate::HasConflict) + .roots() + } else { + start_revset.descendants_at(args.offset) + } + .minus(&wc_revset); + let targets: Vec = target_revset .evaluate_programmatic(workspace_command.repo().as_ref())? .iter() .commits(workspace_command.repo().store()) .try_collect()?; + let target = match targets.as_slice() { [target] => target, [] => { diff --git a/cli/src/commands/obslog.rs b/cli/src/commands/obslog.rs index d3ce56cd80..7a34c735f1 100644 --- a/cli/src/commands/obslog.rs +++ b/cli/src/commands/obslog.rs @@ -39,8 +39,16 @@ pub(crate) struct ObslogArgs { #[arg(long, short, default_value = "@")] revision: RevisionArg, /// Limit number of revisions to show - #[arg(long, short)] + #[arg(long, short = 'n')] limit: Option, + // TODO: Delete `-l` alias in jj 0.25+ + #[arg( + short = 'l', + hide = true, + conflicts_with = "limit", + value_name = "LIMIT" + )] + deprecated_limit: Option, /// Don't show the graph, show a flat list of revisions #[arg(long)] no_graph: bool, @@ -107,7 +115,13 @@ pub(crate) fn cmd_obslog( |commit: &Commit| commit.id().clone(), |commit: &Commit| commit.predecessors().collect_vec(), )?; - if let Some(n) = args.limit { + if args.deprecated_limit.is_some() { + writeln!( + ui.warning_default(), + "The -l shorthand is deprecated, use -n instead." + )?; + } + if let Some(n) = args.limit.or(args.deprecated_limit) { commits.truncate(n); } if !args.no_graph { diff --git a/cli/src/commands/operation.rs b/cli/src/commands/operation.rs deleted file mode 100644 index 782cb85335..0000000000 --- a/cli/src/commands/operation.rs +++ /dev/null @@ -1,409 +0,0 @@ -// Copyright 2020-2023 The Jujutsu Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// https://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use std::io::Write as _; -use std::slice; - -use clap::Subcommand; -use itertools::Itertools as _; -use jj_lib::object_id::ObjectId; -use jj_lib::op_store::OperationId; -use jj_lib::op_walk; -use jj_lib::operation::Operation; -use jj_lib::repo::Repo; - -use crate::cli_util::{format_template, short_operation_hash, CommandHelper, LogContentFormat}; -use crate::command_error::{user_error, user_error_with_hint, CommandError}; -use crate::graphlog::{get_graphlog, Edge}; -use crate::operation_templater::OperationTemplateLanguage; -use crate::ui::Ui; - -/// Commands for working with the operation log -/// -/// For information about the operation log, see -/// https://github.com/martinvonz/jj/blob/main/docs/operation-log.md. -#[derive(Subcommand, Clone, Debug)] -pub enum OperationCommand { - Abandon(OperationAbandonArgs), - Log(OperationLogArgs), - Undo(OperationUndoArgs), - Restore(OperationRestoreArgs), -} - -/// Show the operation log -#[derive(clap::Args, Clone, Debug)] -pub struct OperationLogArgs { - /// Limit number of operations to show - #[arg(long, short)] - limit: Option, - /// Don't show the graph, show a flat list of operations - #[arg(long)] - no_graph: bool, - /// Render each operation using the given template - /// - /// For the syntax, see https://github.com/martinvonz/jj/blob/main/docs/templates.md - #[arg(long, short = 'T')] - template: Option, -} - -/// Create a new operation that restores the repo to an earlier state -/// -/// This restores the repo to the state at the specified operation, effectively -/// undoing all later operations. It does so by creating a new operation. -#[derive(clap::Args, Clone, Debug)] -pub struct OperationRestoreArgs { - /// The operation to restore to - /// - /// Use `jj op log` to find an operation to restore to. Use e.g. `jj - /// --at-op= log` before restoring to an operation to see the - /// state of the repo at that operation. - operation: String, - - /// What portions of the local state to restore (can be repeated) - /// - /// This option is EXPERIMENTAL. - #[arg(long, value_enum, default_values_t = DEFAULT_UNDO_WHAT)] - what: Vec, -} - -/// Create a new operation that undoes an earlier operation -/// -/// This undoes an individual operation by applying the inverse of the -/// operation. -#[derive(clap::Args, Clone, Debug)] -pub struct OperationUndoArgs { - /// The operation to undo - /// - /// Use `jj op log` to find an operation to undo. - #[arg(default_value = "@")] - operation: String, - - /// What portions of the local state to restore (can be repeated) - /// - /// This option is EXPERIMENTAL. - #[arg(long, value_enum, default_values_t = DEFAULT_UNDO_WHAT)] - what: Vec, -} - -/// Abandon operation history -/// -/// To discard old operation history, use `jj op abandon ..`. It -/// will abandon the specified operation and all its ancestors. The descendants -/// will be reparented onto the root operation. -/// -/// To discard recent operations, use `jj op restore ` followed -/// by `jj op abandon ..@-`. -/// -/// The abandoned operations, commits, and other unreachable objects can later -/// be garbage collected by using `jj util gc` command. -#[derive(clap::Args, Clone, Debug)] -pub struct OperationAbandonArgs { - /// The operation or operation range to abandon - operation: String, -} - -#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, clap::ValueEnum)] -enum UndoWhatToRestore { - /// The jj repo state and local branches - Repo, - /// The remote-tracking branches. Do not restore these if you'd like to push - /// after the undo - RemoteTracking, -} - -const DEFAULT_UNDO_WHAT: [UndoWhatToRestore; 2] = - [UndoWhatToRestore::Repo, UndoWhatToRestore::RemoteTracking]; - -fn cmd_op_log( - ui: &mut Ui, - command: &CommandHelper, - args: &OperationLogArgs, -) -> Result<(), CommandError> { - // Don't load the repo so that the operation history can be inspected even - // with a corrupted repo state. For example, you can find the first bad - // operation id to be abandoned. - let workspace = command.load_workspace()?; - let repo_loader = workspace.repo_loader(); - let head_op_str = &command.global_args().at_operation; - let head_ops = if head_op_str == "@" { - // If multiple head ops can't be resolved without merging, let the - // current op be empty. Beware that resolve_op_for_load() will eliminate - // redundant heads whereas get_current_head_ops() won't. - let current_op = op_walk::resolve_op_for_load(repo_loader, head_op_str).ok(); - if let Some(op) = current_op { - vec![op] - } else { - op_walk::get_current_head_ops( - repo_loader.op_store(), - repo_loader.op_heads_store().as_ref(), - )? - } - } else { - vec![op_walk::resolve_op_for_load(repo_loader, head_op_str)?] - }; - let current_op_id = match &*head_ops { - [op] => Some(op.id()), - _ => None, - }; - let with_content_format = LogContentFormat::new(ui, command.settings())?; - - let template; - let op_node_template; - { - let language = OperationTemplateLanguage::new( - repo_loader.op_store().root_operation_id(), - current_op_id, - command.operation_template_extensions(), - ); - let text = match &args.template { - Some(value) => value.to_owned(), - None => command.settings().config().get_string("templates.op_log")?, - }; - template = command - .parse_template( - ui, - &language, - &text, - OperationTemplateLanguage::wrap_operation, - )? - .labeled("op_log"); - op_node_template = command - .parse_template( - ui, - &language, - &command.settings().op_node_template(), - OperationTemplateLanguage::wrap_operation, - )? - .labeled("node"); - } - - ui.request_pager(); - let mut formatter = ui.stdout_formatter(); - let formatter = formatter.as_mut(); - let iter = op_walk::walk_ancestors(&head_ops).take(args.limit.unwrap_or(usize::MAX)); - if !args.no_graph { - let mut graph = get_graphlog(command.settings(), formatter.raw()); - for op in iter { - let op = op?; - let mut edges = vec![]; - for id in op.parent_ids() { - edges.push(Edge::Direct(id.clone())); - } - let mut buffer = vec![]; - with_content_format.write_graph_text( - ui.new_formatter(&mut buffer).as_mut(), - |formatter| template.format(&op, formatter), - || graph.width(op.id(), &edges), - )?; - if !buffer.ends_with(b"\n") { - buffer.push(b'\n'); - } - let node_symbol = format_template(ui, &op, &op_node_template); - graph.add_node( - op.id(), - &edges, - &node_symbol, - &String::from_utf8_lossy(&buffer), - )?; - } - } else { - for op in iter { - let op = op?; - with_content_format.write(formatter, |formatter| template.format(&op, formatter))?; - } - } - - Ok(()) -} - -/// Restore only the portions of the view specified by the `what` argument -fn view_with_desired_portions_restored( - view_being_restored: &jj_lib::op_store::View, - current_view: &jj_lib::op_store::View, - what: &[UndoWhatToRestore], -) -> jj_lib::op_store::View { - let repo_source = if what.contains(&UndoWhatToRestore::Repo) { - view_being_restored - } else { - current_view - }; - let remote_source = if what.contains(&UndoWhatToRestore::RemoteTracking) { - view_being_restored - } else { - current_view - }; - jj_lib::op_store::View { - head_ids: repo_source.head_ids.clone(), - local_branches: repo_source.local_branches.clone(), - tags: repo_source.tags.clone(), - remote_views: remote_source.remote_views.clone(), - git_refs: current_view.git_refs.clone(), - git_head: current_view.git_head.clone(), - wc_commit_ids: repo_source.wc_commit_ids.clone(), - } -} - -pub fn cmd_op_undo( - ui: &mut Ui, - command: &CommandHelper, - args: &OperationUndoArgs, -) -> Result<(), CommandError> { - let mut workspace_command = command.workspace_helper(ui)?; - let bad_op = workspace_command.resolve_single_op(&args.operation)?; - let mut parent_ops = bad_op.parents(); - let Some(parent_op) = parent_ops.next().transpose()? else { - return Err(user_error("Cannot undo repo initialization")); - }; - if parent_ops.next().is_some() { - return Err(user_error("Cannot undo a merge operation")); - } - - let mut tx = workspace_command.start_transaction(); - let repo_loader = tx.base_repo().loader(); - let bad_repo = repo_loader.load_at(&bad_op)?; - let parent_repo = repo_loader.load_at(&parent_op)?; - tx.mut_repo().merge(&bad_repo, &parent_repo); - let new_view = view_with_desired_portions_restored( - tx.repo().view().store_view(), - tx.base_repo().view().store_view(), - &args.what, - ); - tx.mut_repo().set_view(new_view); - tx.finish(ui, format!("undo operation {}", bad_op.id().hex()))?; - - Ok(()) -} - -fn cmd_op_restore( - ui: &mut Ui, - command: &CommandHelper, - args: &OperationRestoreArgs, -) -> Result<(), CommandError> { - let mut workspace_command = command.workspace_helper(ui)?; - let target_op = workspace_command.resolve_single_op(&args.operation)?; - let mut tx = workspace_command.start_transaction(); - let new_view = view_with_desired_portions_restored( - target_op.view()?.store_view(), - tx.base_repo().view().store_view(), - &args.what, - ); - tx.mut_repo().set_view(new_view); - tx.finish(ui, format!("restore to operation {}", target_op.id().hex()))?; - - Ok(()) -} - -fn cmd_op_abandon( - ui: &mut Ui, - command: &CommandHelper, - args: &OperationAbandonArgs, -) -> Result<(), CommandError> { - // Don't load the repo so that this command can be used to recover from - // corrupted repo state. - let mut workspace = command.load_workspace()?; - let repo_loader = workspace.repo_loader(); - let op_store = repo_loader.op_store(); - // It doesn't make sense to create concurrent operations that will be merged - // with the current head. - let head_op_str = &command.global_args().at_operation; - if head_op_str != "@" { - return Err(user_error("--at-op is not respected")); - } - let current_head_op = op_walk::resolve_op_for_load(repo_loader, head_op_str)?; - let resolve_op = |op_str| op_walk::resolve_op_at(op_store, ¤t_head_op, op_str); - let (abandon_root_op, abandon_head_op) = - if let Some((root_op_str, head_op_str)) = args.operation.split_once("..") { - let root_op = if root_op_str.is_empty() { - let id = op_store.root_operation_id(); - let data = op_store.read_operation(id)?; - Operation::new(op_store.clone(), id.clone(), data) - } else { - resolve_op(root_op_str)? - }; - let head_op = if head_op_str.is_empty() { - current_head_op.clone() - } else { - resolve_op(head_op_str)? - }; - (root_op, head_op) - } else { - let op = resolve_op(&args.operation)?; - let parent_ops: Vec<_> = op.parents().try_collect()?; - let parent_op = match parent_ops.len() { - 0 => return Err(user_error("Cannot abandon the root operation")), - 1 => parent_ops.into_iter().next().unwrap(), - _ => return Err(user_error("Cannot abandon a merge operation")), - }; - (parent_op, op) - }; - - if abandon_head_op == current_head_op { - return Err(user_error_with_hint( - "Cannot abandon the current operation", - "Run `jj undo` to revert the current operation, then use `jj op abandon`", - )); - } - - // Reparent descendants, count the number of abandoned operations. - let stats = op_walk::reparent_range( - op_store.as_ref(), - slice::from_ref(&abandon_head_op), - slice::from_ref(¤t_head_op), - &abandon_root_op, - )?; - let [new_head_id]: [OperationId; 1] = stats.new_head_ids.try_into().unwrap(); - if current_head_op.id() == &new_head_id { - writeln!(ui.status(), "Nothing changed.")?; - return Ok(()); - } - writeln!( - ui.status(), - "Abandoned {} operations and reparented {} descendant operations.", - stats.unreachable_count, - stats.rewritten_count, - )?; - repo_loader - .op_heads_store() - .update_op_heads(slice::from_ref(current_head_op.id()), &new_head_id); - // Remap the operation id of the current workspace. If there were any - // concurrent operations, user will need to re-abandon their ancestors. - if !command.global_args().ignore_working_copy { - let mut locked_ws = workspace.start_working_copy_mutation()?; - let old_op_id = locked_ws.locked_wc().old_operation_id(); - if old_op_id != current_head_op.id() { - writeln!( - ui.warning_default(), - "The working copy operation {} is not updated because it differs from the repo {}.", - short_operation_hash(old_op_id), - short_operation_hash(current_head_op.id()), - )?; - } else { - locked_ws.finish(new_head_id)? - } - } - Ok(()) -} - -pub fn cmd_operation( - ui: &mut Ui, - command: &CommandHelper, - subcommand: &OperationCommand, -) -> Result<(), CommandError> { - match subcommand { - OperationCommand::Abandon(args) => cmd_op_abandon(ui, command, args), - OperationCommand::Log(args) => cmd_op_log(ui, command, args), - OperationCommand::Restore(args) => cmd_op_restore(ui, command, args), - OperationCommand::Undo(args) => cmd_op_undo(ui, command, args), - } -} diff --git a/cli/src/commands/operation/abandon.rs b/cli/src/commands/operation/abandon.rs new file mode 100644 index 0000000000..3d252c45e7 --- /dev/null +++ b/cli/src/commands/operation/abandon.rs @@ -0,0 +1,133 @@ +// Copyright 2020-2023 The Jujutsu Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::io::Write as _; +use std::slice; + +use itertools::Itertools as _; +use jj_lib::op_store::OperationId; +use jj_lib::op_walk; +use jj_lib::operation::Operation; + +use crate::cli_util::{short_operation_hash, CommandHelper}; +use crate::command_error::{user_error, user_error_with_hint, CommandError}; +use crate::ui::Ui; + +/// Abandon operation history +/// +/// To discard old operation history, use `jj op abandon ..`. It +/// will abandon the specified operation and all its ancestors. The descendants +/// will be reparented onto the root operation. +/// +/// To discard recent operations, use `jj op restore ` followed +/// by `jj op abandon ..@-`. +/// +/// The abandoned operations, commits, and other unreachable objects can later +/// be garbage collected by using `jj util gc` command. +#[derive(clap::Args, Clone, Debug)] +pub struct OperationAbandonArgs { + /// The operation or operation range to abandon + operation: String, +} + +pub fn cmd_op_abandon( + ui: &mut Ui, + command: &CommandHelper, + args: &OperationAbandonArgs, +) -> Result<(), CommandError> { + // Don't load the repo so that this command can be used to recover from + // corrupted repo state. + let mut workspace = command.load_workspace()?; + let repo_loader = workspace.repo_loader(); + let op_store = repo_loader.op_store(); + // It doesn't make sense to create concurrent operations that will be merged + // with the current head. + let head_op_str = &command.global_args().at_operation; + if head_op_str != "@" { + return Err(user_error("--at-op is not respected")); + } + let current_head_op = op_walk::resolve_op_for_load(repo_loader, head_op_str)?; + let resolve_op = |op_str| op_walk::resolve_op_at(op_store, ¤t_head_op, op_str); + let (abandon_root_op, abandon_head_op) = + if let Some((root_op_str, head_op_str)) = args.operation.split_once("..") { + let root_op = if root_op_str.is_empty() { + let id = op_store.root_operation_id(); + let data = op_store.read_operation(id)?; + Operation::new(op_store.clone(), id.clone(), data) + } else { + resolve_op(root_op_str)? + }; + let head_op = if head_op_str.is_empty() { + current_head_op.clone() + } else { + resolve_op(head_op_str)? + }; + (root_op, head_op) + } else { + let op = resolve_op(&args.operation)?; + let parent_ops: Vec<_> = op.parents().try_collect()?; + let parent_op = match parent_ops.len() { + 0 => return Err(user_error("Cannot abandon the root operation")), + 1 => parent_ops.into_iter().next().unwrap(), + _ => return Err(user_error("Cannot abandon a merge operation")), + }; + (parent_op, op) + }; + + if abandon_head_op == current_head_op { + return Err(user_error_with_hint( + "Cannot abandon the current operation", + "Run `jj undo` to revert the current operation, then use `jj op abandon`", + )); + } + + // Reparent descendants, count the number of abandoned operations. + let stats = op_walk::reparent_range( + op_store.as_ref(), + slice::from_ref(&abandon_head_op), + slice::from_ref(¤t_head_op), + &abandon_root_op, + )?; + let [new_head_id]: [OperationId; 1] = stats.new_head_ids.try_into().unwrap(); + if current_head_op.id() == &new_head_id { + writeln!(ui.status(), "Nothing changed.")?; + return Ok(()); + } + writeln!( + ui.status(), + "Abandoned {} operations and reparented {} descendant operations.", + stats.unreachable_count, + stats.rewritten_count, + )?; + repo_loader + .op_heads_store() + .update_op_heads(slice::from_ref(current_head_op.id()), &new_head_id); + // Remap the operation id of the current workspace. If there were any + // concurrent operations, user will need to re-abandon their ancestors. + if !command.global_args().ignore_working_copy { + let mut locked_ws = workspace.start_working_copy_mutation()?; + let old_op_id = locked_ws.locked_wc().old_operation_id(); + if old_op_id != current_head_op.id() { + writeln!( + ui.warning_default(), + "The working copy operation {} is not updated because it differs from the repo {}.", + short_operation_hash(old_op_id), + short_operation_hash(current_head_op.id()), + )?; + } else { + locked_ws.finish(new_head_id)? + } + } + Ok(()) +} diff --git a/cli/src/commands/operation/log.rs b/cli/src/commands/operation/log.rs new file mode 100644 index 0000000000..d0bc505ff5 --- /dev/null +++ b/cli/src/commands/operation/log.rs @@ -0,0 +1,154 @@ +// Copyright 2020-2023 The Jujutsu Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use jj_lib::op_walk; + +use crate::cli_util::{format_template, CommandHelper, LogContentFormat}; +use crate::command_error::CommandError; +use crate::graphlog::{get_graphlog, Edge}; +use crate::operation_templater::OperationTemplateLanguage; +use crate::ui::Ui; + +/// Show the operation log +#[derive(clap::Args, Clone, Debug)] +pub struct OperationLogArgs { + /// Limit number of operations to show + #[arg(long, short = 'n')] + limit: Option, + // TODO: Delete `-l` alias in jj 0.25+ + #[arg( + short = 'l', + hide = true, + conflicts_with = "limit", + value_name = "LIMIT" + )] + deprecated_limit: Option, + /// Don't show the graph, show a flat list of operations + #[arg(long)] + no_graph: bool, + /// Render each operation using the given template + /// + /// For the syntax, see https://github.com/martinvonz/jj/blob/main/docs/templates.md + #[arg(long, short = 'T')] + template: Option, +} + +pub fn cmd_op_log( + ui: &mut Ui, + command: &CommandHelper, + args: &OperationLogArgs, +) -> Result<(), CommandError> { + // Don't load the repo so that the operation history can be inspected even + // with a corrupted repo state. For example, you can find the first bad + // operation id to be abandoned. + let workspace = command.load_workspace()?; + let repo_loader = workspace.repo_loader(); + let head_op_str = &command.global_args().at_operation; + let head_ops = if head_op_str == "@" { + // If multiple head ops can't be resolved without merging, let the + // current op be empty. Beware that resolve_op_for_load() will eliminate + // redundant heads whereas get_current_head_ops() won't. + let current_op = op_walk::resolve_op_for_load(repo_loader, head_op_str).ok(); + if let Some(op) = current_op { + vec![op] + } else { + op_walk::get_current_head_ops( + repo_loader.op_store(), + repo_loader.op_heads_store().as_ref(), + )? + } + } else { + vec![op_walk::resolve_op_for_load(repo_loader, head_op_str)?] + }; + let current_op_id = match &*head_ops { + [op] => Some(op.id()), + _ => None, + }; + let with_content_format = LogContentFormat::new(ui, command.settings())?; + + let template; + let op_node_template; + { + let language = OperationTemplateLanguage::new( + repo_loader.op_store().root_operation_id(), + current_op_id, + command.operation_template_extensions(), + ); + let text = match &args.template { + Some(value) => value.to_owned(), + None => command.settings().config().get_string("templates.op_log")?, + }; + template = command + .parse_template( + ui, + &language, + &text, + OperationTemplateLanguage::wrap_operation, + )? + .labeled("op_log"); + op_node_template = command + .parse_template( + ui, + &language, + &command.settings().op_node_template(), + OperationTemplateLanguage::wrap_operation, + )? + .labeled("node"); + } + + ui.request_pager(); + let mut formatter = ui.stdout_formatter(); + let formatter = formatter.as_mut(); + if args.deprecated_limit.is_some() { + writeln!( + ui.warning_default(), + "The -l shorthand is deprecated, use -n instead." + )?; + } + let limit = args.limit.or(args.deprecated_limit).unwrap_or(usize::MAX); + let iter = op_walk::walk_ancestors(&head_ops).take(limit); + if !args.no_graph { + let mut graph = get_graphlog(command.settings(), formatter.raw()); + for op in iter { + let op = op?; + let mut edges = vec![]; + for id in op.parent_ids() { + edges.push(Edge::Direct(id.clone())); + } + let mut buffer = vec![]; + with_content_format.write_graph_text( + ui.new_formatter(&mut buffer).as_mut(), + |formatter| template.format(&op, formatter), + || graph.width(op.id(), &edges), + )?; + if !buffer.ends_with(b"\n") { + buffer.push(b'\n'); + } + let node_symbol = format_template(ui, &op, &op_node_template); + graph.add_node( + op.id(), + &edges, + &node_symbol, + &String::from_utf8_lossy(&buffer), + )?; + } + } else { + for op in iter { + let op = op?; + with_content_format.write(formatter, |formatter| template.format(&op, formatter))?; + } + } + + Ok(()) +} diff --git a/cli/src/commands/operation/mod.rs b/cli/src/commands/operation/mod.rs new file mode 100644 index 0000000000..f28b404df8 --- /dev/null +++ b/cli/src/commands/operation/mod.rs @@ -0,0 +1,92 @@ +// Copyright 2020-2023 The Jujutsu Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +mod abandon; +mod log; +mod restore; +pub mod undo; + +use abandon::{cmd_op_abandon, OperationAbandonArgs}; +use clap::Subcommand; +use log::{cmd_op_log, OperationLogArgs}; +use restore::{cmd_op_restore, OperationRestoreArgs}; +use undo::{cmd_op_undo, OperationUndoArgs}; + +use crate::cli_util::CommandHelper; +use crate::command_error::CommandError; +use crate::ui::Ui; + +/// Commands for working with the operation log +/// +/// For information about the operation log, see +/// https://github.com/martinvonz/jj/blob/main/docs/operation-log.md. +#[derive(Subcommand, Clone, Debug)] +pub enum OperationCommand { + Abandon(OperationAbandonArgs), + Log(OperationLogArgs), + Restore(OperationRestoreArgs), + Undo(OperationUndoArgs), +} + +pub fn cmd_operation( + ui: &mut Ui, + command: &CommandHelper, + subcommand: &OperationCommand, +) -> Result<(), CommandError> { + match subcommand { + OperationCommand::Abandon(args) => cmd_op_abandon(ui, command, args), + OperationCommand::Log(args) => cmd_op_log(ui, command, args), + OperationCommand::Restore(args) => cmd_op_restore(ui, command, args), + OperationCommand::Undo(args) => cmd_op_undo(ui, command, args), + } +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, clap::ValueEnum)] +enum UndoWhatToRestore { + /// The jj repo state and local branches + Repo, + /// The remote-tracking branches. Do not restore these if you'd like to push + /// after the undo + RemoteTracking, +} + +const DEFAULT_UNDO_WHAT: [UndoWhatToRestore; 2] = + [UndoWhatToRestore::Repo, UndoWhatToRestore::RemoteTracking]; + +/// Restore only the portions of the view specified by the `what` argument +fn view_with_desired_portions_restored( + view_being_restored: &jj_lib::op_store::View, + current_view: &jj_lib::op_store::View, + what: &[UndoWhatToRestore], +) -> jj_lib::op_store::View { + let repo_source = if what.contains(&UndoWhatToRestore::Repo) { + view_being_restored + } else { + current_view + }; + let remote_source = if what.contains(&UndoWhatToRestore::RemoteTracking) { + view_being_restored + } else { + current_view + }; + jj_lib::op_store::View { + head_ids: repo_source.head_ids.clone(), + local_branches: repo_source.local_branches.clone(), + tags: repo_source.tags.clone(), + remote_views: remote_source.remote_views.clone(), + git_refs: current_view.git_refs.clone(), + git_head: current_view.git_head.clone(), + wc_commit_ids: repo_source.wc_commit_ids.clone(), + } +} diff --git a/cli/src/commands/operation/restore.rs b/cli/src/commands/operation/restore.rs new file mode 100644 index 0000000000..a9c9dc2695 --- /dev/null +++ b/cli/src/commands/operation/restore.rs @@ -0,0 +1,59 @@ +// Copyright 2020-2023 The Jujutsu Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use jj_lib::object_id::ObjectId; + +use super::{view_with_desired_portions_restored, UndoWhatToRestore, DEFAULT_UNDO_WHAT}; +use crate::cli_util::CommandHelper; +use crate::command_error::CommandError; +use crate::ui::Ui; + +/// Create a new operation that restores the repo to an earlier state +/// +/// This restores the repo to the state at the specified operation, effectively +/// undoing all later operations. It does so by creating a new operation. +#[derive(clap::Args, Clone, Debug)] +pub struct OperationRestoreArgs { + /// The operation to restore to + /// + /// Use `jj op log` to find an operation to restore to. Use e.g. `jj + /// --at-op= log` before restoring to an operation to see the + /// state of the repo at that operation. + operation: String, + + /// What portions of the local state to restore (can be repeated) + /// + /// This option is EXPERIMENTAL. + #[arg(long, value_enum, default_values_t = DEFAULT_UNDO_WHAT)] + what: Vec, +} + +pub fn cmd_op_restore( + ui: &mut Ui, + command: &CommandHelper, + args: &OperationRestoreArgs, +) -> Result<(), CommandError> { + let mut workspace_command = command.workspace_helper(ui)?; + let target_op = workspace_command.resolve_single_op(&args.operation)?; + let mut tx = workspace_command.start_transaction(); + let new_view = view_with_desired_portions_restored( + target_op.view()?.store_view(), + tx.base_repo().view().store_view(), + &args.what, + ); + tx.mut_repo().set_view(new_view); + tx.finish(ui, format!("restore to operation {}", target_op.id().hex()))?; + + Ok(()) +} diff --git a/cli/src/commands/operation/undo.rs b/cli/src/commands/operation/undo.rs new file mode 100644 index 0000000000..2468ce62f2 --- /dev/null +++ b/cli/src/commands/operation/undo.rs @@ -0,0 +1,71 @@ +// Copyright 2020-2023 The Jujutsu Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use jj_lib::object_id::ObjectId; +use jj_lib::repo::Repo; + +use super::{view_with_desired_portions_restored, UndoWhatToRestore, DEFAULT_UNDO_WHAT}; +use crate::cli_util::CommandHelper; +use crate::command_error::{user_error, CommandError}; +use crate::ui::Ui; + +/// Create a new operation that undoes an earlier operation +/// +/// This undoes an individual operation by applying the inverse of the +/// operation. +#[derive(clap::Args, Clone, Debug)] +pub struct OperationUndoArgs { + /// The operation to undo + /// + /// Use `jj op log` to find an operation to undo. + #[arg(default_value = "@")] + operation: String, + + /// What portions of the local state to restore (can be repeated) + /// + /// This option is EXPERIMENTAL. + #[arg(long, value_enum, default_values_t = DEFAULT_UNDO_WHAT)] + what: Vec, +} + +pub fn cmd_op_undo( + ui: &mut Ui, + command: &CommandHelper, + args: &OperationUndoArgs, +) -> Result<(), CommandError> { + let mut workspace_command = command.workspace_helper(ui)?; + let bad_op = workspace_command.resolve_single_op(&args.operation)?; + let mut parent_ops = bad_op.parents(); + let Some(parent_op) = parent_ops.next().transpose()? else { + return Err(user_error("Cannot undo repo initialization")); + }; + if parent_ops.next().is_some() { + return Err(user_error("Cannot undo a merge operation")); + } + + let mut tx = workspace_command.start_transaction(); + let repo_loader = tx.base_repo().loader(); + let bad_repo = repo_loader.load_at(&bad_op)?; + let parent_repo = repo_loader.load_at(&parent_op)?; + tx.mut_repo().merge(&bad_repo, &parent_repo); + let new_view = view_with_desired_portions_restored( + tx.repo().view().store_view(), + tx.base_repo().view().store_view(), + &args.what, + ); + tx.mut_repo().set_view(new_view); + tx.finish(ui, format!("undo operation {}", bad_op.id().hex()))?; + + Ok(()) +} diff --git a/cli/src/commands/prev.rs b/cli/src/commands/prev.rs index acd0c100ed..659f116e6c 100644 --- a/cli/src/commands/prev.rs +++ b/cli/src/commands/prev.rs @@ -14,7 +14,7 @@ use itertools::Itertools; use jj_lib::repo::Repo; -use jj_lib::revset::{RevsetExpression, RevsetIteratorExt}; +use jj_lib::revset::{RevsetExpression, RevsetFilterPredicate, RevsetIteratorExt}; use crate::cli_util::{short_commit_hash, CommandHelper}; use crate::command_error::{user_error, CommandError}; @@ -59,6 +59,9 @@ pub(crate) struct PrevArgs { /// Edit the parent directly, instead of moving the working-copy commit. #[arg(long, short)] edit: bool, + /// Jump to the previous conflicted ancestor. + #[arg(long, conflicts_with = "offset")] + conflict: bool, } pub(crate) fn cmd_prev( @@ -76,14 +79,25 @@ pub(crate) fn cmd_prev( .view() .heads() .contains(current_wc_id); + let wc_revset = RevsetExpression::commit(current_wc_id.clone()); // If we're editing, start at the working-copy commit. Otherwise, start from // its direct parent(s). - let target_revset = if edit { - RevsetExpression::commit(current_wc_id.clone()).ancestors_at(args.offset) + let start_revset = if edit { + wc_revset.clone() } else { - RevsetExpression::commit(current_wc_id.clone()) + wc_revset.parents() + }; + + let target_revset = if args.conflict { + // If people desire to move to the root conflict, replace the `heads()` below + // with `roots(). But let's wait for feedback. + start_revset .parents() - .ancestors_at(args.offset) + .ancestors() + .filtered(RevsetFilterPredicate::HasConflict) + .heads() + } else { + start_revset.ancestors_at(args.offset) }; let targets: Vec<_> = target_revset .evaluate_programmatic(workspace_command.repo().as_ref())? diff --git a/cli/src/commands/sparse.rs b/cli/src/commands/sparse.rs index 9b4b79b186..4e04f439de 100644 --- a/cli/src/commands/sparse.rs +++ b/cli/src/commands/sparse.rs @@ -34,11 +34,11 @@ use crate::ui::Ui; /// Manage which paths from the working-copy commit are present in the working /// copy #[derive(Subcommand, Clone, Debug)] -pub(crate) enum SparseArgs { +pub(crate) enum SparseCommand { + Edit(SparseEditArgs), List(SparseListArgs), - Set(SparseSetArgs), Reset(SparseResetArgs), - Edit(SparseEditArgs), + Set(SparseSetArgs), } /// List the patterns that are currently present in the working copy @@ -88,13 +88,13 @@ pub(crate) struct SparseEditArgs {} pub(crate) fn cmd_sparse( ui: &mut Ui, command: &CommandHelper, - args: &SparseArgs, + subcommand: &SparseCommand, ) -> Result<(), CommandError> { - match args { - SparseArgs::List(sub_args) => cmd_sparse_list(ui, command, sub_args), - SparseArgs::Set(sub_args) => cmd_sparse_set(ui, command, sub_args), - SparseArgs::Reset(sub_args) => cmd_sparse_reset(ui, command, sub_args), - SparseArgs::Edit(sub_args) => cmd_sparse_edit(ui, command, sub_args), + match subcommand { + SparseCommand::Edit(args) => cmd_sparse_edit(ui, command, args), + SparseCommand::List(args) => cmd_sparse_list(ui, command, args), + SparseCommand::Reset(args) => cmd_sparse_reset(ui, command, args), + SparseCommand::Set(args) => cmd_sparse_set(ui, command, args), } } diff --git a/cli/src/commands/split.rs b/cli/src/commands/split.rs index eecd1837e6..384193de74 100644 --- a/cli/src/commands/split.rs +++ b/cli/src/commands/split.rs @@ -51,9 +51,11 @@ pub(crate) struct SplitArgs { /// The revision to split #[arg(long, short, default_value = "@")] revision: RevisionArg, - /// Split the revision into two siblings instead of a parent and child. - #[arg(long, short)] - siblings: bool, + /// Split the revision into two parallel revisions instead of a parent and + /// child. + // TODO: Delete `--siblings` alias in jj 0.25+ + #[arg(long, short, alias = "siblings")] + parallel: bool, /// Put these paths in the first commit #[arg(value_hint = clap::ValueHint::AnyPath)] paths: Vec, @@ -138,7 +140,7 @@ the operation will be aborted. // Create the second commit, which includes everything the user didn't // select. - let (second_tree, second_base_tree) = if args.siblings { + let (second_tree, second_base_tree) = if args.parallel { // Merge the original commit tree with its parent using the tree // containing the user selected changes as the base for the merge. // This results in a tree with the changes the user didn't select. @@ -146,7 +148,7 @@ the operation will be aborted. } else { (end_tree, &selected_tree) }; - let second_commit_parents = if args.siblings { + let second_commit_parents = if args.parallel { commit.parent_ids().to_vec() } else { vec![first_commit.id().clone()] @@ -190,11 +192,11 @@ the operation will be aborted. vec![commit.id().clone()], |mut rewriter| { num_rebased += 1; - if args.siblings { + if args.parallel { rewriter .replace_parent(second_commit.id(), [first_commit.id(), second_commit.id()]); } - // We don't need to do anything special for the non-siblings case + // We don't need to do anything special for the non-parallel case // since we already marked the original commit as rewritten. rewriter.rebase(command.settings())?.write()?; Ok(()) diff --git a/cli/src/commands/status.rs b/cli/src/commands/status.rs index 788cd5b640..7eb3eb4f3d 100644 --- a/cli/src/commands/status.rs +++ b/cli/src/commands/status.rs @@ -95,8 +95,10 @@ pub(crate) fn cmd_status( // Ancestors with conflicts, excluding the current working copy commit. let ancestors_conflicts = workspace_command .attach_revset_evaluator( - RevsetExpression::filter(RevsetFilterPredicate::HasConflict) - .intersection(&wc_revset.parents().ancestors()) + wc_revset + .parents() + .ancestors() + .filtered(RevsetFilterPredicate::HasConflict) .minus(&revset_util::parse_immutable_expression( &workspace_command.revset_parse_context(), )?), diff --git a/cli/src/commands/tag.rs b/cli/src/commands/tag.rs index c4f6da2dfc..848d2fb1af 100644 --- a/cli/src/commands/tag.rs +++ b/cli/src/commands/tag.rs @@ -51,7 +51,7 @@ pub fn cmd_tag( subcommand: &TagCommand, ) -> Result<(), CommandError> { match subcommand { - TagCommand::List(sub_args) => cmd_tag_list(ui, command, sub_args), + TagCommand::List(args) => cmd_tag_list(ui, command, args), } } diff --git a/cli/src/commands/untrack.rs b/cli/src/commands/untrack.rs index 6fbee3f975..449724468f 100644 --- a/cli/src/commands/untrack.rs +++ b/cli/src/commands/untrack.rs @@ -69,7 +69,7 @@ pub(crate) fn cmd_untrack( // untracked because they're not ignored. let wc_tree_id = locked_ws.locked_wc().snapshot(SnapshotOptions { base_ignores, - fsmonitor_kind: command.settings().fsmonitor_kind()?, + fsmonitor_settings: command.settings().fsmonitor_settings()?, progress: None, max_new_file_size: command.settings().max_new_file_size()?, })?; diff --git a/cli/src/commands/util.rs b/cli/src/commands/util.rs index 8ddffc8809..54feeb7f84 100644 --- a/cli/src/commands/util.rs +++ b/cli/src/commands/util.rs @@ -134,9 +134,10 @@ fn cmd_util_completion( "`jj util completion --{shell}` will be removed in a future version, and this will be \ a hard error" )?; - if let Some(mut writer) = ui.hint_default() { - writeln!(writer, "Use `jj util completion {shell}` instead")?; - } + writeln!( + ui.hint_default(), + "Use `jj util completion {shell}` instead" + )?; Ok(()) }; let shell = match (args.shell, args.fish, args.zsh, args.bash) { diff --git a/cli/src/config-schema.json b/cli/src/config-schema.json index 25934f95de..5fb08e129f 100644 --- a/cli/src/config-schema.json +++ b/cli/src/config-schema.json @@ -168,6 +168,16 @@ "type": "string", "enum": ["none", "watchman"], "description": "Whether to use an external filesystem monitor, useful for large repos" + }, + "watchman": { + "type": "object", + "properties": { + "register_snapshot_trigger": { + "type": "boolean", + "default": false, + "description": "Whether to use triggers to monitor for changes in the background." + } + } } } }, diff --git a/cli/src/config/colors.toml b/cli/src/config/colors.toml index 286f7fad4b..4a165e3c20 100644 --- a/cli/src/config/colors.toml +++ b/cli/src/config/colors.toml @@ -78,8 +78,9 @@ "diff binary" = "cyan" "diff file_header" = { bold = true } "diff hunk_header" = "cyan" -"diff removed" = "red" -"diff added" = "green" +"diff removed" = { fg = "red" } +"diff added" = { fg = "green" } +"diff token" = { underline = true } "diff modified" = "cyan" "diff access-denied" = { bg = "red" } diff --git a/cli/src/diff_util.rs b/cli/src/diff_util.rs index 587dc67a63..8ac0c15a85 100644 --- a/cli/src/diff_util.rs +++ b/cli/src/diff_util.rs @@ -48,7 +48,7 @@ const DEFAULT_CONTEXT_LINES: usize = 3; #[derive(clap::Args, Clone, Debug)] #[command(next_help_heading = "Diff Formatting Options")] -#[command(group(clap::ArgGroup::new("short-format").args(&["summary", "stat", "types"])))] +#[command(group(clap::ArgGroup::new("short-format").args(&["summary", "stat", "types", "name_only"])))] #[command(group(clap::ArgGroup::new("long-format").args(&["git", "color_words", "tool"])))] pub struct DiffFormatArgs { /// For each path, show only whether it was modified, added, or deleted @@ -66,6 +66,12 @@ pub struct DiffFormatArgs { /// Git submodule. #[arg(long)] pub types: bool, + /// For each path, show only its path + /// + /// Typically useful for shell commands like: + /// `jj diff -r @- --name_only | xargs perl -pi -e's/OLD/NEW/g` + #[arg(long)] + pub name_only: bool, /// Show a Git-format diff #[arg(long)] pub git: bool, @@ -85,6 +91,7 @@ pub enum DiffFormat { Summary, Stat, Types, + NameOnly, Git { context: usize }, ColorWords { context: usize }, Tool(Box), @@ -126,6 +133,7 @@ fn diff_formats_from_args( let mut formats = [ (args.summary, DiffFormat::Summary), (args.types, DiffFormat::Types), + (args.name_only, DiffFormat::NameOnly), ( args.git, DiffFormat::Git { @@ -176,6 +184,7 @@ fn default_diff_format( match name.as_ref() { "summary" => Ok(DiffFormat::Summary), "types" => Ok(DiffFormat::Types), + "name-only" => Ok(DiffFormat::NameOnly), "git" => Ok(DiffFormat::Git { context: num_context_lines.unwrap_or(DEFAULT_CONTEXT_LINES), }), @@ -251,6 +260,10 @@ impl<'a> DiffRenderer<'a> { let tree_diff = from_tree.diff_stream(to_tree, matcher); show_types(formatter, tree_diff, path_converter)?; } + DiffFormat::NameOnly => { + let tree_diff = from_tree.diff_stream(to_tree, matcher); + show_names(formatter, tree_diff, path_converter)?; + } DiffFormat::Git { context } => { let tree_diff = from_tree.diff_stream(to_tree, matcher); show_git_diff(repo, formatter, *context, tree_diff)?; @@ -362,21 +375,25 @@ fn show_color_words_diff_line( diff_line: &DiffLine, ) -> io::Result<()> { if diff_line.has_left_content { - write!( - formatter.labeled("removed"), - "{:>4}", - diff_line.left_line_number - )?; + formatter.with_label("removed", |formatter| { + write!( + formatter.labeled("line_number"), + "{:>4}", + diff_line.left_line_number + ) + })?; write!(formatter, " ")?; } else { write!(formatter, " ")?; } if diff_line.has_right_content { - write!( - formatter.labeled("added"), - "{:>4}", - diff_line.right_line_number - )?; + formatter.with_label("added", |formatter| { + write!( + formatter.labeled("line_number"), + "{:>4}", + diff_line.right_line_number + ) + })?; write!(formatter, ": ")?; } else { write!(formatter, " : ")?; @@ -390,10 +407,14 @@ fn show_color_words_diff_line( let before = data[0]; let after = data[1]; if !before.is_empty() { - formatter.with_label("removed", |formatter| formatter.write_all(before))?; + formatter.with_label("removed", |formatter| { + formatter.with_label("token", |formatter| formatter.write_all(before)) + })?; } if !after.is_empty() { - formatter.with_label("added", |formatter| formatter.write_all(after))?; + formatter.with_label("added", |formatter| { + formatter.with_label("token", |formatter| formatter.write_all(after)) + })?; } } } @@ -726,7 +747,7 @@ fn unified_diff_hunks<'content>( lines: vec![], }; let mut show_context_after = false; - let diff = Diff::for_tokenizer(&[left_content, right_content], &diff::find_line_ranges); + let diff = Diff::for_tokenizer(&[left_content, right_content], diff::find_line_ranges); for hunk in diff.hunks() { match hunk { DiffHunk::Matching(content) => { @@ -1105,3 +1126,17 @@ fn diff_summary_char(value: &MergedTreeValue) -> char { } } } + +pub fn show_names( + formatter: &mut dyn Formatter, + mut tree_diff: TreeDiffStream, + path_converter: &RepoPathUiConverter, +) -> io::Result<()> { + async { + while let Some((repo_path, _)) = tree_diff.next().await { + writeln!(formatter, "{}", path_converter.format_file_path(&repo_path))?; + } + Ok(()) + } + .block_on() +} diff --git a/cli/src/formatter.rs b/cli/src/formatter.rs index 164e4a201d..2d8b97a54a 100644 --- a/cli/src/formatter.rs +++ b/cli/src/formatter.rs @@ -65,6 +65,11 @@ impl LabeledWriter { pub fn new(formatter: T, label: S) -> Self { LabeledWriter { formatter, label } } + + /// Turns into writer that prints labeled message with the `heading`. + pub fn with_heading(self, heading: H) -> HeadingLabeledWriter { + HeadingLabeledWriter::new(self, heading) + } } impl<'a, T, S> LabeledWriter @@ -96,9 +101,9 @@ pub struct HeadingLabeledWriter { } impl HeadingLabeledWriter { - pub fn new(formatter: T, label: S, heading: H) -> Self { + pub fn new(writer: LabeledWriter, heading: H) -> Self { HeadingLabeledWriter { - writer: LabeledWriter::new(formatter, label), + writer, heading: Some(heading), } } @@ -1162,8 +1167,14 @@ mod tests { let mut output: Vec = vec![]; let mut formatter: Box = Box::new(ColorFormatter::for_config(&mut output, &config, false).unwrap()); - HeadingLabeledWriter::new(formatter.as_mut(), "inner", "Should be noop: "); - let mut writer = HeadingLabeledWriter::new(formatter.as_mut(), "inner", "Heading: "); + formatter + .as_mut() + .labeled("inner") + .with_heading("Should be noop: "); + let mut writer = formatter + .as_mut() + .labeled("inner") + .with_heading("Heading: "); write!(writer, "Message").unwrap(); writeln!(writer, " continues").unwrap(); drop(formatter); @@ -1176,7 +1187,10 @@ mod tests { fn test_heading_labeled_writer_empty_string() { let mut output: Vec = vec![]; let mut formatter: Box = Box::new(PlainTextFormatter::new(&mut output)); - let mut writer = HeadingLabeledWriter::new(formatter.as_mut(), "inner", "Heading: "); + let mut writer = formatter + .as_mut() + .labeled("inner") + .with_heading("Heading: "); // write_fmt() is called even if the format string is empty. I don't // know if that's guaranteed, but let's record the current behavior. write!(writer, "").unwrap(); diff --git a/cli/src/git_util.rs b/cli/src/git_util.rs index 05f4ba6192..6642470cef 100644 --- a/cli/src/git_util.rs +++ b/cli/src/git_util.rs @@ -409,14 +409,12 @@ pub fn print_failed_git_export( .iter() .any(|failed| matches!(failed.reason, FailedRefExportReason::FailedToSet(_))) { - if let Some(mut writer) = ui.hint_default() { - writeln!( - writer, - r#"Git doesn't allow a branch name that looks like a parent directory of + writeln!( + ui.hint_default(), + r#"Git doesn't allow a branch name that looks like a parent directory of another (e.g. `foo` and `foo/bar`). Try to rename the branches that failed to export or their "parent" branches."#, - )?; - } + )?; } } Ok(()) diff --git a/cli/src/merge_tools/builtin.rs b/cli/src/merge_tools/builtin.rs index 5d7cd9e0a5..e4c9b424e8 100644 --- a/cli/src/merge_tools/builtin.rs +++ b/cli/src/merge_tools/builtin.rs @@ -227,7 +227,7 @@ fn make_diff_sections( ) -> Result>, BuiltinToolError> { let diff = Diff::for_tokenizer( &[left_contents.as_bytes(), right_contents.as_bytes()], - &find_line_ranges, + find_line_ranges, ); let mut sections = Vec::new(); for hunk in diff.hunks() { @@ -1014,7 +1014,9 @@ mod tests { to_file_id(base_tree.path_value(path).unwrap()), to_file_id(right_tree.path_value(path).unwrap()), ]); - let content = extract_as_single_hunk(&merge, store, path).block_on(); + let content = extract_as_single_hunk(&merge, store, path) + .block_on() + .unwrap(); let slices = content.map(|ContentHunk(buf)| buf.as_slice()); let merge_result = files::merge(&slices); let sections = make_merge_sections(merge_result).unwrap(); diff --git a/cli/src/merge_tools/diff_working_copies.rs b/cli/src/merge_tools/diff_working_copies.rs index 63a4f60c41..d87d4c1cc2 100644 --- a/cli/src/merge_tools/diff_working_copies.rs +++ b/cli/src/merge_tools/diff_working_copies.rs @@ -6,7 +6,7 @@ use std::sync::Arc; use futures::StreamExt; use jj_lib::backend::MergedTreeId; -use jj_lib::fsmonitor::FsmonitorKind; +use jj_lib::fsmonitor::FsmonitorSettings; use jj_lib::gitignore::GitIgnoreFile; use jj_lib::local_working_copy::{TreeState, TreeStateError}; use jj_lib::matchers::Matcher; @@ -279,7 +279,7 @@ diff editing in mind and be a little inaccurate. .unwrap_or(diff_wc.right_tree_state); output_tree_state.snapshot(SnapshotOptions { base_ignores, - fsmonitor_kind: FsmonitorKind::None, + fsmonitor_settings: FsmonitorSettings::None, progress: None, max_new_file_size: u64::MAX, })?; diff --git a/cli/src/merge_tools/mod.rs b/cli/src/merge_tools/mod.rs index 00dee77ba7..091989e95e 100644 --- a/cli/src/merge_tools/mod.rs +++ b/cli/src/merge_tools/mod.rs @@ -120,14 +120,12 @@ fn editor_args_from_settings( Ok(args) } else { let default_editor = BUILTIN_EDITOR_NAME; - if let Some(mut writer) = ui.hint_default() { - writeln!( - writer, - "Using default editor '{default_editor}'; run `jj config set --user {key} \ - :builtin` to disable this message." - ) - .ok(); - } + writeln!( + ui.hint_default(), + "Using default editor '{default_editor}'; run `jj config set --user {key} :builtin` \ + to disable this message." + ) + .ok(); Ok(default_editor.into()) } } @@ -298,14 +296,16 @@ impl MergeEditor { String::from_utf8_lossy(summary_bytes.as_slice()).to_string(), ) })?; + let simplified_file_merge = file_merge.clone().simplify(); // We only support conflicts with 2 sides (3-way conflicts) - if file_merge.num_sides() > 2 { + if simplified_file_merge.num_sides() > 2 { return Err(ConflictResolveError::ConflictTooComplicated { path: repo_path.to_owned(), - sides: file_merge.num_sides(), + sides: simplified_file_merge.num_sides(), }); }; - let content = extract_as_single_hunk(&file_merge, tree.store(), repo_path).block_on(); + let content = + extract_as_single_hunk(&simplified_file_merge, tree.store(), repo_path).block_on()?; match &self.tool { MergeTool::Builtin => { diff --git a/cli/src/revset_util.rs b/cli/src/revset_util.rs index 9655e4e96a..96d3adeb80 100644 --- a/cli/src/revset_util.rs +++ b/cli/src/revset_util.rs @@ -134,17 +134,6 @@ pub fn load_revset_aliases( } } } - - // TODO: If we add support for function overloading (#2966), this check can - // be removed. - let (_, params, _) = aliases_map.get_function(BUILTIN_IMMUTABLE_HEADS).unwrap(); - if !params.is_empty() { - return Err(user_error(format!( - "The `revset-aliases.{name}()` function must be declared without arguments", - name = BUILTIN_IMMUTABLE_HEADS - ))); - } - Ok(aliases_map) } @@ -171,22 +160,26 @@ pub fn default_symbol_resolver<'a>( DefaultSymbolResolver::new(repo, extensions).with_id_prefix_context(id_prefix_context) } -/// Parses user-configured expression defining the immutable set. -pub fn parse_immutable_expression( +/// Parses user-configured expression defining the heads of the immutable set. +/// Includes the root commit. +pub fn parse_immutable_heads_expression( context: &RevsetParseContext, ) -> Result, RevsetParseError> { - let (_, params, immutable_heads_str) = context + let (_, _, immutable_heads_str) = context .aliases_map() - .get_function(BUILTIN_IMMUTABLE_HEADS) + .get_function(BUILTIN_IMMUTABLE_HEADS, 0) .unwrap(); - assert!( - params.is_empty(), - "invalid declaration should have been rejected by load_revset_aliases()" - ); + let heads = revset::parse(immutable_heads_str, context)?; + Ok(heads.union(&RevsetExpression::root())) +} + +/// Parses user-configured expression defining the immutable set. +pub fn parse_immutable_expression( + context: &RevsetParseContext, +) -> Result, RevsetParseError> { // Negated ancestors expression `~::( | root())` is slightly easier // to optimize than negated union `~(:: | root())`. - let heads = revset::parse(immutable_heads_str, context)?; - Ok(heads.union(&RevsetExpression::root()).ancestors()) + Ok(parse_immutable_heads_expression(context)?.ancestors()) } pub(super) fn evaluate_revset_to_single_commit<'a>( diff --git a/cli/src/template_parser.rs b/cli/src/template_parser.rs index 3bb2bb5114..99f418b0e8 100644 --- a/cli/src/template_parser.rs +++ b/cli/src/template_parser.rs @@ -220,7 +220,7 @@ impl AliasExpandError for TemplateParseError { fn within_alias_expansion(self, id: AliasId<'_>, span: pest::Span<'_>) -> Self { let kind = match id { - AliasId::Symbol(_) | AliasId::Function(_) => { + AliasId::Symbol(_) | AliasId::Function(..) => { TemplateParseErrorKind::BadAliasExpansion(id.to_string()) } AliasId::Parameter(_) => TemplateParseErrorKind::BadParameterExpansion(id.to_string()), @@ -1028,16 +1028,36 @@ mod tests { fn test_parse_alias_decl() { let mut aliases_map = TemplateAliasesMap::new(); aliases_map.insert("sym", r#""is symbol""#).unwrap(); - aliases_map.insert("func(a)", r#""is function""#).unwrap(); + aliases_map.insert("func()", r#""is function 0""#).unwrap(); + aliases_map + .insert("func(a, b)", r#""is function 2""#) + .unwrap(); + aliases_map.insert("func(a)", r#""is function a""#).unwrap(); + aliases_map.insert("func(b)", r#""is function b""#).unwrap(); let (id, defn) = aliases_map.get_symbol("sym").unwrap(); assert_eq!(id, AliasId::Symbol("sym")); assert_eq!(defn, r#""is symbol""#); - let (id, params, defn) = aliases_map.get_function("func").unwrap(); - assert_eq!(id, AliasId::Function("func")); - assert_eq!(params, ["a"]); - assert_eq!(defn, r#""is function""#); + let (id, params, defn) = aliases_map.get_function("func", 0).unwrap(); + assert_eq!(id, AliasId::Function("func", &[])); + assert!(params.is_empty()); + assert_eq!(defn, r#""is function 0""#); + + let (id, params, defn) = aliases_map.get_function("func", 1).unwrap(); + assert_eq!(id, AliasId::Function("func", &["b".to_owned()])); + assert_eq!(params, ["b"]); + assert_eq!(defn, r#""is function b""#); + + let (id, params, defn) = aliases_map.get_function("func", 2).unwrap(); + assert_eq!( + id, + AliasId::Function("func", &["a".to_owned(), "b".to_owned()]) + ); + assert_eq!(params, ["a", "b"]); + assert_eq!(defn, r#""is function 2""#); + + assert!(aliases_map.get_function("func", 3).is_none()); // Formal parameter 'a' can't be redefined assert_eq!( @@ -1149,6 +1169,12 @@ mod tests { parse_normalized("a ++ b"), ); + // Not recursion because functions are overloaded by arity. + assert_eq!( + with_aliases([("F(x)", "F(x,b)"), ("F(x,y)", "x ++ y")]).parse_normalized("F(a)"), + parse_normalized("a ++ b") + ); + // Arguments should be resolved in the current scope. assert_eq!( with_aliases([("F(x,y)", "if(x, y)")]).parse_normalized("F(a ++ y, b ++ x)"), @@ -1219,7 +1245,14 @@ mod tests { .parse("F(a)") .unwrap_err() .kind, - TemplateParseErrorKind::BadAliasExpansion("F()".to_owned()), + TemplateParseErrorKind::BadAliasExpansion("F(x)".to_owned()), + ); + assert_eq!( + with_aliases([("F(x)", "F(x,b)"), ("F(x,y)", "F(x|y)")]) + .parse("F(a)") + .unwrap_err() + .kind, + TemplateParseErrorKind::BadAliasExpansion("F(x)".to_owned()) ); } } diff --git a/cli/src/ui.rs b/cli/src/ui.rs index 2f21ed0e14..38dcaef571 100644 --- a/cli/src/ui.rs +++ b/cli/src/ui.rs @@ -23,7 +23,9 @@ use tracing::instrument; use crate::command_error::{config_error_with_message, CommandError}; use crate::config::CommandNameAndArgs; -use crate::formatter::{Formatter, FormatterFactory, HeadingLabeledWriter, LabeledWriter}; +use crate::formatter::{ + Formatter, FormatterFactory, HeadingLabeledWriter, LabeledWriter, PlainTextFormatter, +}; const BUILTIN_PAGER_NAME: &str = ":builtin"; @@ -47,15 +49,15 @@ pub struct BuiltinPager { dynamic_pager_thread: JoinHandle<()>, } -impl std::io::Write for &BuiltinPager { +impl Write for &BuiltinPager { fn flush(&mut self) -> io::Result<()> { // no-op since this is being run in a dynamic pager mode. Ok(()) } fn write(&mut self, buf: &[u8]) -> io::Result { - let string = std::str::from_utf8(buf).map_err(std::io::Error::other)?; - self.pager.push_str(string).map_err(std::io::Error::other)?; + let string = std::str::from_utf8(buf).map_err(io::Error::other)?; + self.pager.push_str(string).map_err(io::Error::other)?; Ok(buf.len()) } } @@ -395,7 +397,7 @@ impl Ui { /// Writer to print an update that's not part of the command's main output. pub fn status(&self) -> Box { if self.quiet { - Box::new(std::io::sink()) + Box::new(io::sink()) } else { Box::new(self.stderr()) } @@ -410,21 +412,24 @@ impl Ui { /// Writer to print hint with the default "Hint: " heading. pub fn hint_default( &self, - ) -> Option, &'static str, &'static str>> { + ) -> HeadingLabeledWriter, &'static str, &'static str> { self.hint_with_heading("Hint: ") } /// Writer to print hint without the "Hint: " heading. - pub fn hint_no_heading(&self) -> Option, &'static str>> { - (!self.quiet).then(|| LabeledWriter::new(self.stderr_formatter(), "hint")) + pub fn hint_no_heading(&self) -> LabeledWriter, &'static str> { + let formatter = self + .status_formatter() + .unwrap_or_else(|| Box::new(PlainTextFormatter::new(io::sink()))); + LabeledWriter::new(formatter, "hint") } /// Writer to print hint with the given heading. pub fn hint_with_heading( &self, heading: H, - ) -> Option, &'static str, H>> { - (!self.quiet).then(|| HeadingLabeledWriter::new(self.stderr_formatter(), "hint", heading)) + ) -> HeadingLabeledWriter, &'static str, H> { + self.hint_no_heading().with_heading(heading) } /// Writer to print warning with the default "Warning: " heading. @@ -444,7 +449,7 @@ impl Ui { &self, heading: H, ) -> HeadingLabeledWriter, &'static str, H> { - HeadingLabeledWriter::new(self.stderr_formatter(), "warning", heading) + self.warning_no_heading().with_heading(heading) } /// Writer to print error without the "Error: " heading. @@ -457,7 +462,7 @@ impl Ui { &self, heading: H, ) -> HeadingLabeledWriter, &'static str, H> { - HeadingLabeledWriter::new(self.stderr_formatter(), "error", heading) + self.error_no_heading().with_heading(heading) } /// Waits for the pager exits. diff --git a/cli/tests/cli-reference@.md.snap b/cli/tests/cli-reference@.md.snap index 49ab95d811..516da42ffa 100644 --- a/cli/tests/cli-reference@.md.snap +++ b/cli/tests/cli-reference@.md.snap @@ -1,6 +1,6 @@ --- source: cli/tests/test_generate_md_cli_help.rs -description: "AUTO-GENERATED FILE, DO NOT EDIT. This cli reference is generated as an `insta` snapshot. MkDocs follows they symlink from docs/cli-reference.md to the snap. Unfortunately, `insta` unavoidably creates this header. Luckily, MkDocs ignores the header since it has the same format as Markdown headers. TODO: MkDocs may fail on Windows if symlinks are not enabled in the OS settings" +description: "AUTO-GENERATED FILE, DO NOT EDIT. This cli reference is generated by a test as an `insta` snapshot. MkDocs includes this snapshot from docs/cli-reference.md." --- @@ -18,38 +18,41 @@ This document contains the help content for the `jj` command-line program. * [`jj branch delete`↴](#jj-branch-delete) * [`jj branch forget`↴](#jj-branch-forget) * [`jj branch list`↴](#jj-branch-list) +* [`jj branch move`↴](#jj-branch-move) * [`jj branch rename`↴](#jj-branch-rename) * [`jj branch set`↴](#jj-branch-set) * [`jj branch track`↴](#jj-branch-track) * [`jj branch untrack`↴](#jj-branch-untrack) -* [`jj cat`↴](#jj-cat) -* [`jj chmod`↴](#jj-chmod) * [`jj commit`↴](#jj-commit) * [`jj config`↴](#jj-config) -* [`jj config list`↴](#jj-config-list) -* [`jj config get`↴](#jj-config-get) -* [`jj config set`↴](#jj-config-set) * [`jj config edit`↴](#jj-config-edit) +* [`jj config get`↴](#jj-config-get) +* [`jj config list`↴](#jj-config-list) * [`jj config path`↴](#jj-config-path) +* [`jj config set`↴](#jj-config-set) * [`jj describe`↴](#jj-describe) * [`jj diff`↴](#jj-diff) * [`jj diffedit`↴](#jj-diffedit) * [`jj duplicate`↴](#jj-duplicate) * [`jj edit`↴](#jj-edit) -* [`jj files`↴](#jj-files) +* [`jj file`↴](#jj-file) +* [`jj file chmod`↴](#jj-file-chmod) +* [`jj file list`↴](#jj-file-list) +* [`jj file show`↴](#jj-file-show) * [`jj fix`↴](#jj-fix) * [`jj git`↴](#jj-git) +* [`jj git clone`↴](#jj-git-clone) +* [`jj git export`↴](#jj-git-export) +* [`jj git fetch`↴](#jj-git-fetch) +* [`jj git import`↴](#jj-git-import) +* [`jj git init`↴](#jj-git-init) +* [`jj git push`↴](#jj-git-push) * [`jj git remote`↴](#jj-git-remote) * [`jj git remote add`↴](#jj-git-remote-add) +* [`jj git remote list`↴](#jj-git-remote-list) * [`jj git remote remove`↴](#jj-git-remote-remove) * [`jj git remote rename`↴](#jj-git-remote-rename) -* [`jj git remote list`↴](#jj-git-remote-list) -* [`jj git init`↴](#jj-git-init) -* [`jj git fetch`↴](#jj-git-fetch) -* [`jj git clone`↴](#jj-git-clone) -* [`jj git push`↴](#jj-git-push) -* [`jj git import`↴](#jj-git-import) -* [`jj git export`↴](#jj-git-export) +* [`jj git remote set-url`↴](#jj-git-remote-set-url) * [`jj init`↴](#jj-init) * [`jj interdiff`↴](#jj-interdiff) * [`jj log`↴](#jj-log) @@ -59,8 +62,8 @@ This document contains the help content for the `jj` command-line program. * [`jj operation`↴](#jj-operation) * [`jj operation abandon`↴](#jj-operation-abandon) * [`jj operation log`↴](#jj-operation-log) -* [`jj operation undo`↴](#jj-operation-undo) * [`jj operation restore`↴](#jj-operation-restore) +* [`jj operation undo`↴](#jj-operation-undo) * [`jj parallelize`↴](#jj-parallelize) * [`jj prev`↴](#jj-prev) * [`jj rebase`↴](#jj-rebase) @@ -69,10 +72,10 @@ This document contains the help content for the `jj` command-line program. * [`jj root`↴](#jj-root) * [`jj show`↴](#jj-show) * [`jj sparse`↴](#jj-sparse) +* [`jj sparse edit`↴](#jj-sparse-edit) * [`jj sparse list`↴](#jj-sparse-list) -* [`jj sparse set`↴](#jj-sparse-set) * [`jj sparse reset`↴](#jj-sparse-reset) -* [`jj sparse edit`↴](#jj-sparse-edit) +* [`jj sparse set`↴](#jj-sparse-set) * [`jj split`↴](#jj-split) * [`jj squash`↴](#jj-squash) * [`jj status`↴](#jj-status) @@ -108,8 +111,6 @@ To get started, see the tutorial at https://github.com/martinvonz/jj/blob/main/d * `abandon` — Abandon a revision * `backout` — Apply the reverse of a revision on top of another revision * `branch` — Manage branches -* `cat` — Print contents of files in a revision -* `chmod` — Sets or removes the executable bit for paths in the repo * `commit` — Update the description and create a new change on top * `config` — Manage config options * `describe` — Update the change description or other metadata @@ -117,7 +118,7 @@ To get started, see the tutorial at https://github.com/martinvonz/jj/blob/main/d * `diffedit` — Touch up the content changes in a revision with a diff editor * `duplicate` — Create a new change with the same content as an existing one * `edit` — Sets the specified revision as the working-copy revision -* `files` — List files in a revision +* `file` — File operations * `fix` — Update files with formatting fixes or other changes * `git` — Commands for working with Git remotes and the underlying Git repo * `init` — Create a new repo in the given directory @@ -149,30 +150,37 @@ To get started, see the tutorial at https://github.com/martinvonz/jj/blob/main/d ###### **Options:** * `-R`, `--repository ` — Path to repository to operate on + + By default, Jujutsu searches for the closest .jj/ directory in an ancestor of the current working directory. * `--ignore-working-copy` — Don't snapshot the working copy, and don't update it - Possible values: `true`, `false` + By default, Jujutsu snapshots the working copy at the beginning of every command. The working copy is also updated at the end of the command, if the command modified the working-copy commit (`@`). If you want to avoid snapshotting the working copy and instead see a possibly stale working copy commit, you can use `--ignore-working-copy`. This may be useful e.g. in a command prompt, especially if you have another process that commits the working copy. + Loading the repository at a specific operation with `--at-operation` implies `--ignore-working-copy`. * `--ignore-immutable` — Allow rewriting immutable commits - Possible values: `true`, `false` + By default, Jujutsu prevents rewriting commits in the configured set of immutable commits. This option disables that check and lets you rewrite any commit but the root commit. + This option only affects the check. It does not affect the `immutable_heads()` revset or the `immutable` template keyword. * `--at-operation ` — Operation to load the repo at - Default value: `@` -* `--debug` — Enable debug logging + Operation to load the repo at. By default, Jujutsu loads the repo at the most recent operation. You can use `--at-op=` to see what the repo looked like at an earlier operation. For example `jj --at-op= st` will show you what `jj st` would have shown you when the given operation had just finished. + + Use `jj op log` to find the operation ID you want. Any unambiguous prefix of the operation ID is enough. - Possible values: `true`, `false` + When loading the repo at an earlier operation, the working copy will be ignored, as if `--ignore-working-copy` had been specified. + It is possible to run mutating commands when loading the repo at an earlier operation. Doing that is equivalent to having run concurrent commands starting at the earlier operation. There's rarely a reason to do that, but it is possible. + + Default value: `@` +* `--debug` — Enable debug logging * `--color ` — When to colorize output (always, never, debug, auto) * `--quiet` — Silence non-primary command output - Possible values: `true`, `false` + For example, `jj file list ` will still list files, but it won't tell you if the working copy was snapshotted or if descendants were rebased. + Warnings and errors will still be printed. * `--no-pager` — Disable the pager - - Possible values: `true`, `false` - * `--config-toml ` — Additional configuration options (can be repeated) @@ -197,10 +205,6 @@ If a working-copy commit gets abandoned, it will be given a new, empty commit. T * `-s`, `--summary` — Do not print every abandoned commit on a separate line - Possible values: `true`, `false` - -* `-r` — Ignored (but lets you pass `-r` for consistency with other commands) - ## `jj backout` @@ -222,7 +226,7 @@ Apply the reverse of a revision on top of another revision ## `jj branch` -Manage branches. +Manage branches For information about branches, see https://github.com/martinvonz/jj/blob/main/docs/branches.md. @@ -234,8 +238,9 @@ For information about branches, see https://github.com/martinvonz/jj/blob/main/d * `delete` — Delete an existing branch and propagate the deletion to remotes on the next push * `forget` — Forget everything about a branch, including its local and remote targets * `list` — List branches and their targets +* `move` — Move existing branches to target revision * `rename` — Rename `old` branch name to `new` branch name -* `set` — Update an existing branch to point to a certain commit +* `set` — Create or update a branch to point to a certain commit * `track` — Start tracking given remote branches * `untrack` — Stop tracking given remote branches @@ -261,33 +266,29 @@ Create a new branch Delete an existing branch and propagate the deletion to remotes on the next push -**Usage:** `jj branch delete [NAMES]...` +**Usage:** `jj branch delete ...` ###### **Arguments:** * `` — The branches to delete -###### **Options:** - -* `--glob ` — Deprecated. Please prefix the pattern with `glob:` instead + By default, the specified name matches exactly. Use `glob:` prefix to select branches by wildcard pattern. For details, see https://github.com/martinvonz/jj/blob/main/docs/revsets.md#string-patterns. ## `jj branch forget` -Forget everything about a branch, including its local and remote targets. +Forget everything about a branch, including its local and remote targets A forgotten branch will not impact remotes on future pushes. It will be recreated on future pulls if it still exists in the remote. -**Usage:** `jj branch forget [NAMES]...` +**Usage:** `jj branch forget ...` ###### **Arguments:** * `` — The branches to forget -###### **Options:** - -* `--glob ` — Deprecated. Please prefix the pattern with `glob:` instead + By default, the specified name matches exactly. Use `glob:` prefix to select branches by wildcard pattern. For details, see https://github.com/martinvonz/jj/blob/main/docs/revsets.md#string-patterns. @@ -305,28 +306,57 @@ For information about branches, see https://github.com/martinvonz/jj/blob/main/d * `` — Show branches whose local name matches + By default, the specified name matches exactly. Use `glob:` prefix to select branches by wildcard pattern. For details, see https://github.com/martinvonz/jj/blob/main/docs/revsets.md#string-patterns. + ###### **Options:** * `-a`, `--all-remotes` — Show all tracking and non-tracking remote branches including the ones whose targets are synchronized with the local branches +* `-t`, `--tracked` — Show remote tracked branches only. Omits local Git-tracking branches by default +* `-c`, `--conflicted` — Show conflicted branches only +* `-r`, `--revisions ` — Show branches whose local targets are in the given revisions - Possible values: `true`, `false` + Note that `-r deleted_branch` will not work since `deleted_branch` wouldn't have a local target. +* `-T`, `--template