├── .cursorrules ├── Cargo.toml ├── .gitignore ├── LICENSE ├── src ├── task.rs ├── github.rs ├── config.rs ├── git.rs └── main.rs ├── .github └── workflows │ └── release.yml ├── README.md └── Cargo.lock /.cursorrules: -------------------------------------------------------------------------------- 1 | # Project Rules 2 | 1. All user-facing messages in the code should be in English. -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "gh-glance" 3 | version = "0.2.1" 4 | edition = "2021" 5 | authors = ["Minoru Takeuchi"] 6 | description = "A GitHub CLI extension for managing PR worktrees" 7 | license = "MIT" 8 | 9 | [dependencies] 10 | clap = { version = "4.4", features = ["derive"] } 11 | serde = { version = "1.0", features = ["derive"] } 12 | toml = "0.8" 13 | anyhow = "1.0" 14 | tokio = { version = "1.0", features = ["full"] } 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | debug/ 4 | target/ 5 | 6 | # These are backup files generated by rustfmt 7 | **/*.rs.bk 8 | 9 | # MSVC Windows builds of rustc generate these, which store debugging information 10 | *.pdb 11 | 12 | # RustRover 13 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 14 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 15 | # and can be added to the global gitignore or merged into this file. For a more nuclear 16 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 17 | #.idea/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Minoru Takeuchi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/task.rs: -------------------------------------------------------------------------------- 1 | use crate::config::Config; 2 | use anyhow::{Context, Result}; 3 | use std::process::Command; 4 | 5 | pub struct TaskRunner<'a> { 6 | config: &'a Config, 7 | workdir: String, 8 | } 9 | 10 | impl<'a> TaskRunner<'a> { 11 | pub fn new(config: &'a Config, workdir: String) -> Self { 12 | Self { config, workdir } 13 | } 14 | 15 | pub fn run(&self, task_name: &str) -> Result<()> { 16 | let task = self.config.get_task(task_name) 17 | .context(format!("Task '{}' not found", task_name))?; 18 | 19 | // 準備タスクの実行(もし設定されていれば) 20 | if task_name != self.config.base.prepare_task { 21 | if let Some(prepare_task) = self.config.get_task(&self.config.base.prepare_task) { 22 | self.execute_command(&prepare_task.run)?; 23 | } 24 | } 25 | 26 | // メインタスクの実行 27 | self.execute_command(&task.run)?; 28 | 29 | Ok(()) 30 | } 31 | 32 | pub fn execute_command(&self, cmd: &str) -> Result<()> { 33 | let status = if cfg!(target_os = "windows") { 34 | Command::new("cmd") 35 | .args(["/C", cmd]) 36 | .current_dir(&self.workdir) 37 | .status() 38 | } else { 39 | Command::new("sh") 40 | .args(["-c", cmd]) 41 | .current_dir(&self.workdir) 42 | .status() 43 | }.context("Failed to execute command")?; 44 | 45 | if !status.success() { 46 | anyhow::bail!("Command failed with exit code: {}", status); 47 | } 48 | 49 | Ok(()) 50 | } 51 | } -------------------------------------------------------------------------------- /src/github.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{Context, Result}; 2 | use std::process::Command; 3 | 4 | pub struct GitHub {} 5 | 6 | impl GitHub { 7 | pub async fn new() -> Result { 8 | Ok(Self {}) 9 | } 10 | 11 | pub async fn get_pr_branch(&self, pr_number: u64) -> Result { 12 | let output = Command::new("gh") 13 | .args(["pr", "view", &pr_number.to_string(), "--json", "headRefName", "--jq", ".headRefName"]) 14 | .output() 15 | .context("Failed to execute gh command")?; 16 | 17 | if !output.status.success() { 18 | anyhow::bail!("Failed to get PR branch: {}", String::from_utf8_lossy(&output.stderr)); 19 | } 20 | 21 | let branch = String::from_utf8(output.stdout) 22 | .context("Failed to parse gh command output")? 23 | .trim() 24 | .to_string(); 25 | 26 | Ok(branch) 27 | } 28 | 29 | pub async fn is_pr_merged(&self, pr_number: u64) -> Result { 30 | let output = Command::new("gh") 31 | .args(["pr", "view", &pr_number.to_string(), "--json", "state", "--jq", ".state"]) 32 | .output() 33 | .context("Failed to execute gh command")?; 34 | 35 | if !output.status.success() { 36 | return Ok(false); 37 | } 38 | 39 | let state = String::from_utf8(output.stdout) 40 | .context("Failed to parse gh command output")? 41 | .trim() 42 | .to_string(); 43 | 44 | Ok(state == "MERGED") 45 | } 46 | 47 | pub fn extract_pr_number_from_worktree(&self, worktree: &str) -> Option { 48 | let path = std::path::Path::new(worktree); 49 | path.file_name() 50 | .and_then(|name| name.to_str()) 51 | .and_then(|name| name.parse::().ok()) 52 | } 53 | } -------------------------------------------------------------------------------- /src/config.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{Context, Result}; 2 | use serde::Deserialize; 3 | use std::collections::HashMap; 4 | use std::fs; 5 | use std::path::Path; 6 | 7 | #[derive(Debug, Deserialize)] 8 | pub struct Config { 9 | #[serde(default)] 10 | pub base: BaseConfig, 11 | #[serde(default)] 12 | pub tasks: HashMap, 13 | } 14 | 15 | #[derive(Debug, Deserialize)] 16 | pub struct BaseConfig { 17 | #[serde(default)] 18 | pub prepare_task: String, 19 | #[serde(default = "default_auto_pull")] 20 | pub auto_pull: String, 21 | #[serde(default = "default_worktree_dir")] 22 | pub worktree_dir: String, 23 | #[serde(default = "default_auto_checkout")] 24 | pub auto_checkout: bool, 25 | } 26 | 27 | #[derive(Debug, Deserialize)] 28 | pub struct TaskConfig { 29 | pub run: String, 30 | } 31 | 32 | impl Config { 33 | pub fn load() -> Result { 34 | let config_path = Path::new(".gh-glance.toml"); 35 | 36 | // 設定ファイルが存在しない場合はデフォルト設定を使用 37 | if !config_path.exists() { 38 | return Ok(Self { 39 | base: BaseConfig::default(), 40 | tasks: HashMap::new(), 41 | }); 42 | } 43 | 44 | let content = fs::read_to_string(config_path) 45 | .context("Failed to read config file")?; 46 | toml::from_str(&content).context("Failed to parse config file") 47 | } 48 | 49 | pub fn get_task(&self, name: &str) -> Option<&TaskConfig> { 50 | self.tasks.get(name) 51 | } 52 | } 53 | 54 | impl Default for BaseConfig { 55 | fn default() -> Self { 56 | Self { 57 | prepare_task: String::new(), 58 | auto_pull: default_auto_pull(), 59 | worktree_dir: default_worktree_dir(), 60 | auto_checkout: default_auto_checkout(), 61 | } 62 | } 63 | } 64 | 65 | fn default_auto_pull() -> String { 66 | "default".to_string() 67 | } 68 | 69 | fn default_worktree_dir() -> String { 70 | ".worktree".to_string() 71 | } 72 | 73 | fn default_auto_checkout() -> bool { 74 | true 75 | } -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*" 7 | 8 | permissions: 9 | contents: write 10 | 11 | jobs: 12 | release: 13 | name: Release binary for multi OS 14 | strategy: 15 | matrix: 16 | include: 17 | - os: macos-13 18 | target: x86_64-apple-darwin 19 | artifact: darwin-amd64 20 | - os: macos-13 21 | target: aarch64-apple-darwin 22 | artifact: darwin-arm64 23 | - os: windows-2022 24 | target: x86_64-pc-windows-msvc 25 | artifact: windows-amd64.exe 26 | - os: windows-2022 27 | target: aarch64-pc-windows-msvc 28 | artifact: windows-arm64.exe 29 | - os: ubuntu-24.04 30 | target: x86_64-unknown-linux-gnu 31 | artifact: linux-amd64 32 | - os: ubuntu-24.04 33 | target: aarch64-unknown-linux-gnu 34 | artifact: linux-arm64 35 | runs-on: ${{ matrix.os }} 36 | steps: 37 | - uses: actions/checkout@v4 38 | - uses: actions-rs/toolchain@v1 39 | with: 40 | toolchain: stable 41 | target: ${{ matrix.target }} 42 | override: true 43 | - uses: actions-rs/cargo@v1 44 | with: 45 | use-cross: true 46 | command: build 47 | args: --release --target ${{ matrix.target }} 48 | - name: Package extension 49 | shell: bash 50 | if: matrix.os != 'windows-2022' 51 | run: | 52 | mkdir -p dist 53 | cp target/${{ matrix.target }}/release/gh-glance dist/gh-glance-${{ matrix.artifact }} 54 | - name: Package extension (Windows) 55 | if: matrix.os == 'windows-2022' 56 | shell: pwsh 57 | run: | 58 | New-Item -ItemType Directory -Force -Path dist 59 | Copy-Item target\${{ matrix.target }}\release\gh-glance.exe dist\gh-glance-${{ matrix.artifact }} 60 | - name: Upload artifacts 61 | uses: actions/upload-artifact@v4 62 | with: 63 | name: binaries-${{ matrix.artifact }} 64 | path: dist/* 65 | 66 | create-release: 67 | name: Create Release 68 | needs: release 69 | runs-on: ubuntu-24.04 70 | permissions: 71 | contents: write 72 | steps: 73 | - uses: actions/checkout@v4 74 | - uses: actions/download-artifact@v4 75 | with: 76 | pattern: binaries-* 77 | path: dist 78 | merge-multiple: true 79 | - name: Create Release 80 | env: 81 | GH_TOKEN: ${{ github.token }} 82 | run: | 83 | gh release create ${{ github.ref_name }} \ 84 | --title ${{ github.ref_name }} \ 85 | --generate-notes \ 86 | dist/* 87 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gh-glance 2 | 3 | GitHub CLI extension for quickly checking (glancing at) PRs using worktrees. 4 | 5 | ## Motivation 6 | 7 | When reviewing a pull request, you often need to test it locally. However, switching branches requires committing your local changes first. 8 | 9 | [`git worktree`](https://git-scm.com/docs/git-worktree) solves this by letting you check out a branch in a separate directory while keeping your working branch untouched. 10 | 11 | One caveat is that files listed in `.gitignore` (like `node_modules`) aren't present in the new worktree. For a Node.js project, you'd typically need to: 12 | 13 | ```shell 14 | git worktree add ./.worktree/feature-branch origin/feature-branch 15 | cd ./.worktree/feature-branch 16 | npm install 17 | npm run dev 18 | ``` 19 | 20 | `gh-glance` simplifies this process into a single command that handles all the necessary worktree setup: 21 | 22 | ```shell 23 | gh glance run 1234 dev 24 | ``` 25 | 26 | ## Installation 27 | 28 | ```shell 29 | gh extension install dora1998/gh-glance 30 | ``` 31 | 32 | Make sure to add the worktree root directory (default: `.worktree/`) to your `.gitignore`: 33 | 34 | ``` 35 | .worktree/ 36 | ``` 37 | 38 | ## Usage 39 | 40 | ### `run` 41 | 42 | Run defined tasks in the pr's worktree. You can also omit `run`. 43 | You can specify either pr number or branch name. 44 | 45 | ```shell 46 | # Pull request number 47 | gh glance run 1234 storybook 48 | # Branch name 49 | gh glance run feature/foo storybook 50 | # Omit `run` 51 | gh glance 1234 storybook 52 | ``` 53 | 54 | Run any command with `--`. 55 | 56 | ```shell 57 | gh glance 1234 -- pwd 58 | ``` 59 | 60 | ### `dir` 61 | 62 | Get the PR's worktree directory path. Useful with `cd` command: 63 | 64 | ```shell 65 | cd $(gh glance dir 1234) 66 | ``` 67 | 68 | ### `checkout` 69 | 70 | Add the pr's worktree. 71 | 72 | ```shell 73 | gh glance checkout 1234 74 | ``` 75 | 76 | ### `remove` 77 | 78 | Remove the pr's worktree. 79 | 80 | ```shell 81 | gh glance rm 1234 82 | ``` 83 | 84 | ### `clean` 85 | 86 | Remove all merged branch's worktrees. 87 | 88 | ```shell 89 | gh glance clean 90 | ``` 91 | 92 | ## Configuration 93 | 94 | Configuration should be written in `.gh-glance.toml` file placed in the project root directory. 95 | 96 | Below is an example configuration: 97 | 98 | ```toml 99 | [base] 100 | worktree_dir = ".worktree/" 101 | prepare_task = "prepare" 102 | auto_checkout = false 103 | auto_pull = "force" 104 | 105 | [tasks.prepare] 106 | run = "cp .env.sample .env.local && bun i" 107 | 108 | [tasks.dev] 109 | run = "bun dev" 110 | 111 | [tasks.storybook] 112 | run = "bun storybook" 113 | ``` 114 | 115 | ### `base.worktree_dir` 116 | 117 | - Type: String 118 | - Default: ".worktree/" 119 | - Description: Directory name for worktree root. 120 | 121 | ### `base.prepare_task` 122 | 123 | - Type: String 124 | - Default: "" (empty string) 125 | - Description: Task name to run before each task. This task will be executed before running any other task, except when running the prepare task itself. 126 | 127 | ### `base.auto_checkout` 128 | 129 | - Type: Boolean 130 | - Default: true 131 | - Description: Whether to automatically checkout the branch when running a task. 132 | 133 | ### `base.auto_pull` 134 | 135 | - Type: String 136 | - Values: "default" | "force" | "off" 137 | - Default: "default" 138 | - Description: Determines how to update the worktree when checking out 139 | - `default`: Performs a normal `git pull` 140 | - `force`: Performs `git fetch` followed by `git reset --hard origin/` 141 | - `off`: Skips any update operation 142 | 143 | ### `tasks..run` 144 | 145 | - Type: String 146 | - Description: Command to run for this task. The command will be executed in the worktree directory. 147 | 148 | ## Development 149 | 150 | ### Prerequisites 151 | 152 | - Rust toolchain (1.70.0 or later) 153 | - GitHub CLI 154 | - GitHub Personal Access Token (with `repo` scope) 155 | 156 | ### Build 157 | 158 | ```shell 159 | cargo build 160 | ``` 161 | 162 | ### Run the project locally 163 | 164 | ```shell 165 | cargo run -- checkout 1234 # Checkout PR #1234 166 | cargo run -- run 1234 dev # Run dev task in PR #1234's worktree 167 | ``` 168 | -------------------------------------------------------------------------------- /src/git.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{Context, Result}; 2 | use std::process::Command; 3 | use std::path::Path; 4 | 5 | pub struct Git; 6 | 7 | impl Git { 8 | pub fn new() -> Self { 9 | Self 10 | } 11 | 12 | pub fn get_repo_root(&self) -> Result { 13 | let output = Command::new("git") 14 | .args(["rev-parse", "--show-toplevel"]) 15 | .output() 16 | .context("Failed to get repository root")?; 17 | 18 | if !output.status.success() { 19 | anyhow::bail!("Failed to get repository root: {}", String::from_utf8_lossy(&output.stderr)); 20 | } 21 | 22 | Ok(String::from_utf8(output.stdout) 23 | .context("Invalid UTF-8")? 24 | .trim() 25 | .to_string()) 26 | } 27 | 28 | pub fn exists_worktree(&self, path: &str) -> bool { 29 | let worktrees = self.list_worktrees().unwrap_or_default(); 30 | let repo_root = self.get_repo_root().unwrap_or_default(); 31 | let target_path = Path::new(&repo_root).join(path); 32 | worktrees.iter().any(|w| Path::new(w) == target_path) 33 | } 34 | 35 | pub fn add_worktree(&self, branch: &str, path: &str) -> Result<()> { 36 | if self.exists_worktree(path) { 37 | return Ok(()); 38 | } 39 | 40 | println!("Adding worktree for branch '{}' at '{}'", branch, path); 41 | let repo_root = self.get_repo_root()?; 42 | let output = Command::new("git") 43 | .args(["worktree", "add", path, branch]) 44 | .current_dir(&repo_root) 45 | .output() 46 | .context("Failed to add worktree")?; 47 | 48 | if !output.status.success() { 49 | anyhow::bail!("Failed to add worktree: {}", String::from_utf8_lossy(&output.stderr)); 50 | } 51 | 52 | Ok(()) 53 | } 54 | 55 | pub fn remove_worktree(&self, path: &str) -> Result<()> { 56 | if !self.exists_worktree(path) { 57 | anyhow::bail!("Worktree not found at: {}", path); 58 | } 59 | 60 | let repo_root = self.get_repo_root()?; 61 | Command::new("git") 62 | .args(["worktree", "remove", path]) 63 | .current_dir(&repo_root) 64 | .status() 65 | .context("Failed to remove worktree")?; 66 | Ok(()) 67 | } 68 | 69 | pub fn list_worktrees(&self) -> Result> { 70 | let repo_root = self.get_repo_root()?; 71 | let output = Command::new("git") 72 | .args(["worktree", "list", "--porcelain"]) 73 | .current_dir(&repo_root) 74 | .output() 75 | .context("Failed to list worktrees")?; 76 | 77 | let output = String::from_utf8(output.stdout).context("Invalid UTF-8")?; 78 | Ok(output 79 | .lines() 80 | .filter(|line| line.starts_with("worktree ")) 81 | .map(|line| line[9..].to_string()) 82 | .collect()) 83 | } 84 | 85 | pub fn pull(&self, path: &str, branch: &str, mode: &str) -> Result<()> { 86 | let repo_root = self.get_repo_root()?; 87 | let full_path = Path::new(&repo_root).join(path); 88 | if !full_path.exists() { 89 | return Ok(()); 90 | } 91 | 92 | // 追跡ブランチの取得 93 | let output = Command::new("git") 94 | .args(["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}"]) 95 | .current_dir(&full_path) 96 | .output() 97 | .context("Failed to get upstream branch")?; 98 | 99 | if !output.status.success() { 100 | // 追跡ブランチが設定されていない場合は何もしない 101 | return Ok(()); 102 | } 103 | 104 | let upstream = String::from_utf8(output.stdout) 105 | .context("Invalid UTF-8")? 106 | .trim() 107 | .to_string(); 108 | 109 | // 追跡ブランチの存在確認 110 | let output = Command::new("git") 111 | .args(["rev-parse", "--verify", &upstream]) 112 | .current_dir(&full_path) 113 | .output() 114 | .context("Failed to check remote branch")?; 115 | 116 | if !output.status.success() { 117 | // リモートブランチが存在しない場合は何もしない 118 | return Ok(()); 119 | } 120 | 121 | match mode { 122 | "default" => { 123 | Command::new("git") 124 | .args(["pull"]) 125 | .current_dir(&full_path) 126 | .status() 127 | .context("Failed to pull changes")?; 128 | } 129 | "force" => { 130 | Command::new("git") 131 | .args(["fetch"]) 132 | .current_dir(&full_path) 133 | .status() 134 | .context("Failed to fetch changes")?; 135 | 136 | Command::new("git") 137 | .args(["reset", "--hard", &format!("origin/{}", branch)]) 138 | .current_dir(&full_path) 139 | .status() 140 | .context("Failed to reset branch")?; 141 | } 142 | "off" => { 143 | // 何もしない 144 | return Ok(()); 145 | } 146 | _ => { 147 | anyhow::bail!("Invalid auto_pull mode: {}", mode); 148 | } 149 | } 150 | Ok(()) 151 | } 152 | 153 | pub fn is_branch_merged(&self, path: &str) -> Result { 154 | let repo_root = self.get_repo_root()?; 155 | let full_path = Path::new(&repo_root).join(path); 156 | 157 | // mainブランチの最新コミットを取得 158 | let output = Command::new("git") 159 | .args(["rev-parse", "main"]) 160 | .current_dir(&full_path) 161 | .output() 162 | .context("Failed to get main branch commit")?; 163 | 164 | if !output.status.success() { 165 | return Ok(false); 166 | } 167 | 168 | let main_commit = String::from_utf8(output.stdout)?.trim().to_string(); 169 | 170 | // 現在のブランチのコミットがmainブランチに含まれているかチェック 171 | let output = Command::new("git") 172 | .args(["merge-base", "--is-ancestor", "HEAD", &main_commit]) 173 | .current_dir(&full_path) 174 | .status() 175 | .context("Failed to check if branch is merged")?; 176 | 177 | Ok(output.success()) 178 | } 179 | 180 | pub fn can_fast_forward(&self, path: &str) -> Result { 181 | let repo_root = self.get_repo_root()?; 182 | let full_path = Path::new(&repo_root).join(path); 183 | 184 | // リモートの最新状態を取得 185 | Command::new("git") 186 | .args(["fetch"]) 187 | .current_dir(&full_path) 188 | .status() 189 | .context("Failed to fetch")?; 190 | 191 | // 現在のブランチの追跡ブランチを取得 192 | let output = Command::new("git") 193 | .args(["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}"]) 194 | .current_dir(&full_path) 195 | .output() 196 | .context("Failed to get upstream branch")?; 197 | 198 | if !output.status.success() { 199 | return Ok(false); 200 | } 201 | 202 | let upstream = String::from_utf8(output.stdout)?.trim().to_string(); 203 | 204 | // fast-forwardマージ可能かチェック 205 | let output = Command::new("git") 206 | .args(["merge-base", "--is-ancestor", "HEAD", &upstream]) 207 | .current_dir(&full_path) 208 | .status() 209 | .context("Failed to check if can fast-forward")?; 210 | 211 | Ok(output.success()) 212 | } 213 | } -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | mod config; 2 | mod git; 3 | mod github; 4 | mod task; 5 | 6 | use anyhow::{Context, Result}; 7 | use clap::{Parser, Subcommand}; 8 | use config::Config; 9 | use git::Git; 10 | use github::GitHub; 11 | use task::TaskRunner; 12 | use std::path::Path; 13 | use std::io::{self, Write}; 14 | 15 | #[derive(Parser)] 16 | #[command(author, version, about, long_about = None)] 17 | struct Cli { 18 | #[command(subcommand)] 19 | command: Option, 20 | 21 | /// PR number or branch name 22 | #[arg(global = true)] 23 | target: Option, 24 | 25 | /// Task name to run 26 | #[arg(global = true)] 27 | task: Option, 28 | 29 | /// Command to run after -- 30 | #[arg(last = true)] 31 | command_args: Vec, 32 | } 33 | 34 | #[derive(Subcommand)] 35 | enum Commands { 36 | /// Run tasks in PR's worktree 37 | Run { 38 | /// PR number or branch name 39 | target: String, 40 | /// Task name 41 | task: String, 42 | }, 43 | /// Get PR's worktree directory 44 | Dir { 45 | /// PR number or branch name 46 | target: String, 47 | }, 48 | /// Move to PR's worktree 49 | Checkout { 50 | /// PR number or branch name 51 | target: String, 52 | }, 53 | /// Remove PR's worktree 54 | Rm { 55 | /// PR number or branch name 56 | target: String, 57 | }, 58 | /// Remove all merged branch's worktrees 59 | Clean, 60 | } 61 | 62 | #[tokio::main] 63 | async fn main() -> Result<()> { 64 | let cli = Cli::parse(); 65 | let git = Git::new(); 66 | let github = GitHub::new().await?; 67 | 68 | // Handle direct command with -- 69 | if !cli.command_args.is_empty() { 70 | if let Some(target) = cli.target { 71 | run_direct_command(&target, &cli.command_args)?; 72 | return Ok(()); 73 | } 74 | } 75 | 76 | match cli.command { 77 | Some(Commands::Run { target, task }) => { 78 | run_task(&target, &task).await?; 79 | } 80 | Some(Commands::Dir { target }) => { 81 | get_worktree_dir(&git, &github, &target).await?; 82 | } 83 | Some(Commands::Checkout { target }) => { 84 | checkout_target(&git, &github, &target).await?; 85 | } 86 | Some(Commands::Rm { target }) => { 87 | remove_target(&git, &target)?; 88 | } 89 | Some(Commands::Clean) => { 90 | clean_worktrees(&git, &github).await?; 91 | } 92 | None => { 93 | if let (Some(target), Some(task)) = (cli.target, cli.task) { 94 | run_task(&target, &task).await?; 95 | } 96 | } 97 | } 98 | 99 | Ok(()) 100 | } 101 | 102 | async fn run_task(target: &str, task: &str) -> Result<()> { 103 | let config = Config::load()?; 104 | let workdir = Path::new(&config.base.worktree_dir).join(target); 105 | let git = Git::new(); 106 | let github = GitHub::new().await?; 107 | 108 | let workdir_str = workdir.to_str() 109 | .with_context(|| format!("Invalid characters in worktree path: {}", workdir.display()))?; 110 | 111 | // auto_checkoutが有効な場合は、ワークツリーが存在しない場合に自動的にチェックアウトする 112 | if config.base.auto_checkout && !git.exists_worktree(workdir_str) { 113 | checkout_target(&git, &github, target).await?; 114 | } else if !git.exists_worktree(workdir_str) { 115 | anyhow::bail!("Worktree not found at: {}. Please run 'checkout' first.", workdir.display()); 116 | } 117 | 118 | let runner = TaskRunner::new(&config, workdir_str.to_string()); 119 | 120 | // タスクが定義されていない場合はエラーを返す 121 | if config.get_task(task).is_some() { 122 | runner.run(task) 123 | } else { 124 | anyhow::bail!("Task '{}' is not defined in the configuration file", task) 125 | } 126 | } 127 | 128 | fn run_direct_command(target: &str, args: &[String]) -> Result<()> { 129 | let config = Config::load()?; 130 | let workdir = Path::new(&config.base.worktree_dir).join(target); 131 | let git = Git::new(); 132 | 133 | let workdir_str = workdir.to_str() 134 | .with_context(|| format!("Invalid characters in worktree path: {}", workdir.display()))?; 135 | 136 | if !git.exists_worktree(workdir_str) { 137 | anyhow::bail!("Worktree not found at: {}. Please run 'checkout' first.", workdir.display()); 138 | } 139 | 140 | let runner = TaskRunner::new(&config, workdir_str.to_string()); 141 | runner.execute_command(&args.join(" ")) 142 | } 143 | 144 | async fn checkout_target(git: &Git, github: &GitHub, target: &str) -> Result<()> { 145 | let config = Config::load()?; 146 | let workdir = Path::new(&config.base.worktree_dir).join(target); 147 | let workdir_str = workdir.to_str() 148 | .with_context(|| format!("Invalid characters in worktree path: {}", workdir.display()))?; 149 | 150 | // 既存のワークツリーをチェック 151 | if git.exists_worktree(workdir_str) { 152 | println!("Worktree already exists at: {}", workdir.display()); 153 | return Ok(()); 154 | } 155 | 156 | // PRの場合はブランチ名を取得 157 | let branch = if let Ok(pr_number) = target.parse::() { 158 | github.get_pr_branch(pr_number).await? 159 | } else { 160 | target.to_string() 161 | }; 162 | 163 | git.add_worktree(&branch, workdir_str)?; 164 | 165 | // auto_pullの設定に応じてプル 166 | if config.base.auto_pull != "off" { 167 | git.pull(workdir_str, &branch, &config.base.auto_pull)?; 168 | } 169 | 170 | Ok(()) 171 | } 172 | 173 | fn remove_target(git: &Git, target: &str) -> Result<()> { 174 | let config = Config::load()?; 175 | let workdir = Path::new(&config.base.worktree_dir).join(target); 176 | let workdir_str = workdir.to_str() 177 | .with_context(|| format!("Invalid characters in worktree path: {}", workdir.display()))?; 178 | git.remove_worktree(workdir_str) 179 | } 180 | 181 | async fn clean_worktrees(git: &Git, github: &GitHub) -> Result<()> { 182 | let config = Config::load()?; 183 | let base_dir = Path::new(&config.base.worktree_dir); 184 | let worktrees = git.list_worktrees()?; 185 | let mut to_remove = Vec::new(); 186 | 187 | // 削除対象のworktreeを収集 188 | for worktree in worktrees { 189 | let worktree_path = Path::new(&worktree); 190 | if !worktree_path.starts_with(base_dir) { 191 | continue; 192 | } 193 | 194 | // PRの場合はGitHubのAPIでマージ状態を確認 195 | let is_merged = if let Some(pr_number) = github.extract_pr_number_from_worktree(&worktree) { 196 | github.is_pr_merged(pr_number).await? 197 | } else { 198 | // PRでない場合は従来通りgitコマンドで確認 199 | git.is_branch_merged(&worktree)? 200 | }; 201 | 202 | // マージ済みかつfast-forwardでマージ可能なworktreeのみを対象とする 203 | if is_merged && git.can_fast_forward(&worktree)? { 204 | to_remove.push(worktree); 205 | } 206 | } 207 | 208 | if to_remove.is_empty() { 209 | println!("No worktrees to remove."); 210 | return Ok(()); 211 | } 212 | 213 | // 削除対象の一覧を表示 214 | println!("\nThe following worktrees will be removed:"); 215 | for worktree in &to_remove { 216 | println!(" - {}", worktree); 217 | } 218 | 219 | // ユーザーに確認 220 | print!("\nDo you want to continue? [y/N]: "); 221 | io::stdout().flush()?; 222 | 223 | let mut input = String::new(); 224 | io::stdin().read_line(&mut input)?; 225 | 226 | if input.trim().to_lowercase() == "y" { 227 | // 確認が取れたら削除を実行 228 | for worktree in to_remove { 229 | println!("Removing worktree: {}", worktree); 230 | git.remove_worktree(&worktree)?; 231 | } 232 | println!("\nRemoval completed."); 233 | } else { 234 | println!("\nOperation cancelled."); 235 | } 236 | 237 | Ok(()) 238 | } 239 | 240 | async fn get_worktree_dir(git: &Git, github: &GitHub, target: &str) -> Result<()> { 241 | let config = Config::load()?; 242 | let repo_root = git.get_repo_root()?; 243 | let workdir = Path::new(&repo_root).join(&config.base.worktree_dir).join(target); 244 | let workdir_str = workdir.to_str() 245 | .with_context(|| format!("Invalid characters in worktree path: {}", workdir.display()))?; 246 | 247 | // auto_checkoutが有効な場合は、ワークツリーが存在しない場合に自動的にチェックアウトする 248 | if config.base.auto_checkout && !git.exists_worktree(workdir_str) { 249 | checkout_target(git, github, target).await?; 250 | } else if !git.exists_worktree(workdir_str) { 251 | anyhow::bail!("Worktree not found at: {}. Please run 'checkout' first.", workdir.display()); 252 | } 253 | 254 | println!("{}", workdir_str); 255 | Ok(()) 256 | } -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "addr2line" 7 | version = "0.24.2" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" 10 | dependencies = [ 11 | "gimli", 12 | ] 13 | 14 | [[package]] 15 | name = "adler2" 16 | version = "2.0.0" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" 19 | 20 | [[package]] 21 | name = "anstream" 22 | version = "0.6.18" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" 25 | dependencies = [ 26 | "anstyle", 27 | "anstyle-parse", 28 | "anstyle-query", 29 | "anstyle-wincon", 30 | "colorchoice", 31 | "is_terminal_polyfill", 32 | "utf8parse", 33 | ] 34 | 35 | [[package]] 36 | name = "anstyle" 37 | version = "1.0.10" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" 40 | 41 | [[package]] 42 | name = "anstyle-parse" 43 | version = "0.2.6" 44 | source = "registry+https://github.com/rust-lang/crates.io-index" 45 | checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" 46 | dependencies = [ 47 | "utf8parse", 48 | ] 49 | 50 | [[package]] 51 | name = "anstyle-query" 52 | version = "1.1.2" 53 | source = "registry+https://github.com/rust-lang/crates.io-index" 54 | checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" 55 | dependencies = [ 56 | "windows-sys 0.59.0", 57 | ] 58 | 59 | [[package]] 60 | name = "anstyle-wincon" 61 | version = "3.0.7" 62 | source = "registry+https://github.com/rust-lang/crates.io-index" 63 | checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" 64 | dependencies = [ 65 | "anstyle", 66 | "once_cell", 67 | "windows-sys 0.59.0", 68 | ] 69 | 70 | [[package]] 71 | name = "anyhow" 72 | version = "1.0.95" 73 | source = "registry+https://github.com/rust-lang/crates.io-index" 74 | checksum = "34ac096ce696dc2fcabef30516bb13c0a68a11d30131d3df6f04711467681b04" 75 | 76 | [[package]] 77 | name = "autocfg" 78 | version = "1.4.0" 79 | source = "registry+https://github.com/rust-lang/crates.io-index" 80 | checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" 81 | 82 | [[package]] 83 | name = "backtrace" 84 | version = "0.3.74" 85 | source = "registry+https://github.com/rust-lang/crates.io-index" 86 | checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" 87 | dependencies = [ 88 | "addr2line", 89 | "cfg-if", 90 | "libc", 91 | "miniz_oxide", 92 | "object", 93 | "rustc-demangle", 94 | "windows-targets", 95 | ] 96 | 97 | [[package]] 98 | name = "bitflags" 99 | version = "2.8.0" 100 | source = "registry+https://github.com/rust-lang/crates.io-index" 101 | checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36" 102 | 103 | [[package]] 104 | name = "bytes" 105 | version = "1.10.0" 106 | source = "registry+https://github.com/rust-lang/crates.io-index" 107 | checksum = "f61dac84819c6588b558454b194026eb1f09c293b9036ae9b159e74e73ab6cf9" 108 | 109 | [[package]] 110 | name = "cfg-if" 111 | version = "1.0.0" 112 | source = "registry+https://github.com/rust-lang/crates.io-index" 113 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 114 | 115 | [[package]] 116 | name = "clap" 117 | version = "4.5.29" 118 | source = "registry+https://github.com/rust-lang/crates.io-index" 119 | checksum = "8acebd8ad879283633b343856142139f2da2317c96b05b4dd6181c61e2480184" 120 | dependencies = [ 121 | "clap_builder", 122 | "clap_derive", 123 | ] 124 | 125 | [[package]] 126 | name = "clap_builder" 127 | version = "4.5.29" 128 | source = "registry+https://github.com/rust-lang/crates.io-index" 129 | checksum = "f6ba32cbda51c7e1dfd49acc1457ba1a7dec5b64fe360e828acb13ca8dc9c2f9" 130 | dependencies = [ 131 | "anstream", 132 | "anstyle", 133 | "clap_lex", 134 | "strsim", 135 | ] 136 | 137 | [[package]] 138 | name = "clap_derive" 139 | version = "4.5.28" 140 | source = "registry+https://github.com/rust-lang/crates.io-index" 141 | checksum = "bf4ced95c6f4a675af3da73304b9ac4ed991640c36374e4b46795c49e17cf1ed" 142 | dependencies = [ 143 | "heck", 144 | "proc-macro2", 145 | "quote", 146 | "syn", 147 | ] 148 | 149 | [[package]] 150 | name = "clap_lex" 151 | version = "0.7.4" 152 | source = "registry+https://github.com/rust-lang/crates.io-index" 153 | checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" 154 | 155 | [[package]] 156 | name = "colorchoice" 157 | version = "1.0.3" 158 | source = "registry+https://github.com/rust-lang/crates.io-index" 159 | checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" 160 | 161 | [[package]] 162 | name = "equivalent" 163 | version = "1.0.2" 164 | source = "registry+https://github.com/rust-lang/crates.io-index" 165 | checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" 166 | 167 | [[package]] 168 | name = "gh-glance" 169 | version = "0.2.1" 170 | dependencies = [ 171 | "anyhow", 172 | "clap", 173 | "serde", 174 | "tokio", 175 | "toml", 176 | ] 177 | 178 | [[package]] 179 | name = "gimli" 180 | version = "0.31.1" 181 | source = "registry+https://github.com/rust-lang/crates.io-index" 182 | checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" 183 | 184 | [[package]] 185 | name = "hashbrown" 186 | version = "0.15.2" 187 | source = "registry+https://github.com/rust-lang/crates.io-index" 188 | checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" 189 | 190 | [[package]] 191 | name = "heck" 192 | version = "0.5.0" 193 | source = "registry+https://github.com/rust-lang/crates.io-index" 194 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 195 | 196 | [[package]] 197 | name = "indexmap" 198 | version = "2.7.1" 199 | source = "registry+https://github.com/rust-lang/crates.io-index" 200 | checksum = "8c9c992b02b5b4c94ea26e32fe5bccb7aa7d9f390ab5c1221ff895bc7ea8b652" 201 | dependencies = [ 202 | "equivalent", 203 | "hashbrown", 204 | ] 205 | 206 | [[package]] 207 | name = "is_terminal_polyfill" 208 | version = "1.70.1" 209 | source = "registry+https://github.com/rust-lang/crates.io-index" 210 | checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" 211 | 212 | [[package]] 213 | name = "libc" 214 | version = "0.2.169" 215 | source = "registry+https://github.com/rust-lang/crates.io-index" 216 | checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" 217 | 218 | [[package]] 219 | name = "lock_api" 220 | version = "0.4.12" 221 | source = "registry+https://github.com/rust-lang/crates.io-index" 222 | checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" 223 | dependencies = [ 224 | "autocfg", 225 | "scopeguard", 226 | ] 227 | 228 | [[package]] 229 | name = "memchr" 230 | version = "2.7.4" 231 | source = "registry+https://github.com/rust-lang/crates.io-index" 232 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 233 | 234 | [[package]] 235 | name = "miniz_oxide" 236 | version = "0.8.4" 237 | source = "registry+https://github.com/rust-lang/crates.io-index" 238 | checksum = "b3b1c9bd4fe1f0f8b387f6eb9eb3b4a1aa26185e5750efb9140301703f62cd1b" 239 | dependencies = [ 240 | "adler2", 241 | ] 242 | 243 | [[package]] 244 | name = "mio" 245 | version = "1.0.3" 246 | source = "registry+https://github.com/rust-lang/crates.io-index" 247 | checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" 248 | dependencies = [ 249 | "libc", 250 | "wasi", 251 | "windows-sys 0.52.0", 252 | ] 253 | 254 | [[package]] 255 | name = "object" 256 | version = "0.36.7" 257 | source = "registry+https://github.com/rust-lang/crates.io-index" 258 | checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" 259 | dependencies = [ 260 | "memchr", 261 | ] 262 | 263 | [[package]] 264 | name = "once_cell" 265 | version = "1.20.3" 266 | source = "registry+https://github.com/rust-lang/crates.io-index" 267 | checksum = "945462a4b81e43c4e3ba96bd7b49d834c6f61198356aa858733bc4acf3cbe62e" 268 | 269 | [[package]] 270 | name = "parking_lot" 271 | version = "0.12.3" 272 | source = "registry+https://github.com/rust-lang/crates.io-index" 273 | checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" 274 | dependencies = [ 275 | "lock_api", 276 | "parking_lot_core", 277 | ] 278 | 279 | [[package]] 280 | name = "parking_lot_core" 281 | version = "0.9.10" 282 | source = "registry+https://github.com/rust-lang/crates.io-index" 283 | checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" 284 | dependencies = [ 285 | "cfg-if", 286 | "libc", 287 | "redox_syscall", 288 | "smallvec", 289 | "windows-targets", 290 | ] 291 | 292 | [[package]] 293 | name = "pin-project-lite" 294 | version = "0.2.16" 295 | source = "registry+https://github.com/rust-lang/crates.io-index" 296 | checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" 297 | 298 | [[package]] 299 | name = "proc-macro2" 300 | version = "1.0.93" 301 | source = "registry+https://github.com/rust-lang/crates.io-index" 302 | checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99" 303 | dependencies = [ 304 | "unicode-ident", 305 | ] 306 | 307 | [[package]] 308 | name = "quote" 309 | version = "1.0.38" 310 | source = "registry+https://github.com/rust-lang/crates.io-index" 311 | checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" 312 | dependencies = [ 313 | "proc-macro2", 314 | ] 315 | 316 | [[package]] 317 | name = "redox_syscall" 318 | version = "0.5.8" 319 | source = "registry+https://github.com/rust-lang/crates.io-index" 320 | checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834" 321 | dependencies = [ 322 | "bitflags", 323 | ] 324 | 325 | [[package]] 326 | name = "rustc-demangle" 327 | version = "0.1.24" 328 | source = "registry+https://github.com/rust-lang/crates.io-index" 329 | checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" 330 | 331 | [[package]] 332 | name = "scopeguard" 333 | version = "1.2.0" 334 | source = "registry+https://github.com/rust-lang/crates.io-index" 335 | checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 336 | 337 | [[package]] 338 | name = "serde" 339 | version = "1.0.217" 340 | source = "registry+https://github.com/rust-lang/crates.io-index" 341 | checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70" 342 | dependencies = [ 343 | "serde_derive", 344 | ] 345 | 346 | [[package]] 347 | name = "serde_derive" 348 | version = "1.0.217" 349 | source = "registry+https://github.com/rust-lang/crates.io-index" 350 | checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" 351 | dependencies = [ 352 | "proc-macro2", 353 | "quote", 354 | "syn", 355 | ] 356 | 357 | [[package]] 358 | name = "serde_spanned" 359 | version = "0.6.8" 360 | source = "registry+https://github.com/rust-lang/crates.io-index" 361 | checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" 362 | dependencies = [ 363 | "serde", 364 | ] 365 | 366 | [[package]] 367 | name = "signal-hook-registry" 368 | version = "1.4.2" 369 | source = "registry+https://github.com/rust-lang/crates.io-index" 370 | checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" 371 | dependencies = [ 372 | "libc", 373 | ] 374 | 375 | [[package]] 376 | name = "smallvec" 377 | version = "1.14.0" 378 | source = "registry+https://github.com/rust-lang/crates.io-index" 379 | checksum = "7fcf8323ef1faaee30a44a340193b1ac6814fd9b7b4e88e9d4519a3e4abe1cfd" 380 | 381 | [[package]] 382 | name = "socket2" 383 | version = "0.5.8" 384 | source = "registry+https://github.com/rust-lang/crates.io-index" 385 | checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8" 386 | dependencies = [ 387 | "libc", 388 | "windows-sys 0.52.0", 389 | ] 390 | 391 | [[package]] 392 | name = "strsim" 393 | version = "0.11.1" 394 | source = "registry+https://github.com/rust-lang/crates.io-index" 395 | checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" 396 | 397 | [[package]] 398 | name = "syn" 399 | version = "2.0.98" 400 | source = "registry+https://github.com/rust-lang/crates.io-index" 401 | checksum = "36147f1a48ae0ec2b5b3bc5b537d267457555a10dc06f3dbc8cb11ba3006d3b1" 402 | dependencies = [ 403 | "proc-macro2", 404 | "quote", 405 | "unicode-ident", 406 | ] 407 | 408 | [[package]] 409 | name = "tokio" 410 | version = "1.43.0" 411 | source = "registry+https://github.com/rust-lang/crates.io-index" 412 | checksum = "3d61fa4ffa3de412bfea335c6ecff681de2b609ba3c77ef3e00e521813a9ed9e" 413 | dependencies = [ 414 | "backtrace", 415 | "bytes", 416 | "libc", 417 | "mio", 418 | "parking_lot", 419 | "pin-project-lite", 420 | "signal-hook-registry", 421 | "socket2", 422 | "tokio-macros", 423 | "windows-sys 0.52.0", 424 | ] 425 | 426 | [[package]] 427 | name = "tokio-macros" 428 | version = "2.5.0" 429 | source = "registry+https://github.com/rust-lang/crates.io-index" 430 | checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" 431 | dependencies = [ 432 | "proc-macro2", 433 | "quote", 434 | "syn", 435 | ] 436 | 437 | [[package]] 438 | name = "toml" 439 | version = "0.8.20" 440 | source = "registry+https://github.com/rust-lang/crates.io-index" 441 | checksum = "cd87a5cdd6ffab733b2f74bc4fd7ee5fff6634124999ac278c35fc78c6120148" 442 | dependencies = [ 443 | "serde", 444 | "serde_spanned", 445 | "toml_datetime", 446 | "toml_edit", 447 | ] 448 | 449 | [[package]] 450 | name = "toml_datetime" 451 | version = "0.6.8" 452 | source = "registry+https://github.com/rust-lang/crates.io-index" 453 | checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" 454 | dependencies = [ 455 | "serde", 456 | ] 457 | 458 | [[package]] 459 | name = "toml_edit" 460 | version = "0.22.24" 461 | source = "registry+https://github.com/rust-lang/crates.io-index" 462 | checksum = "17b4795ff5edd201c7cd6dca065ae59972ce77d1b80fa0a84d94950ece7d1474" 463 | dependencies = [ 464 | "indexmap", 465 | "serde", 466 | "serde_spanned", 467 | "toml_datetime", 468 | "winnow", 469 | ] 470 | 471 | [[package]] 472 | name = "unicode-ident" 473 | version = "1.0.16" 474 | source = "registry+https://github.com/rust-lang/crates.io-index" 475 | checksum = "a210d160f08b701c8721ba1c726c11662f877ea6b7094007e1ca9a1041945034" 476 | 477 | [[package]] 478 | name = "utf8parse" 479 | version = "0.2.2" 480 | source = "registry+https://github.com/rust-lang/crates.io-index" 481 | checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" 482 | 483 | [[package]] 484 | name = "wasi" 485 | version = "0.11.0+wasi-snapshot-preview1" 486 | source = "registry+https://github.com/rust-lang/crates.io-index" 487 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 488 | 489 | [[package]] 490 | name = "windows-sys" 491 | version = "0.52.0" 492 | source = "registry+https://github.com/rust-lang/crates.io-index" 493 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 494 | dependencies = [ 495 | "windows-targets", 496 | ] 497 | 498 | [[package]] 499 | name = "windows-sys" 500 | version = "0.59.0" 501 | source = "registry+https://github.com/rust-lang/crates.io-index" 502 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 503 | dependencies = [ 504 | "windows-targets", 505 | ] 506 | 507 | [[package]] 508 | name = "windows-targets" 509 | version = "0.52.6" 510 | source = "registry+https://github.com/rust-lang/crates.io-index" 511 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 512 | dependencies = [ 513 | "windows_aarch64_gnullvm", 514 | "windows_aarch64_msvc", 515 | "windows_i686_gnu", 516 | "windows_i686_gnullvm", 517 | "windows_i686_msvc", 518 | "windows_x86_64_gnu", 519 | "windows_x86_64_gnullvm", 520 | "windows_x86_64_msvc", 521 | ] 522 | 523 | [[package]] 524 | name = "windows_aarch64_gnullvm" 525 | version = "0.52.6" 526 | source = "registry+https://github.com/rust-lang/crates.io-index" 527 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 528 | 529 | [[package]] 530 | name = "windows_aarch64_msvc" 531 | version = "0.52.6" 532 | source = "registry+https://github.com/rust-lang/crates.io-index" 533 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 534 | 535 | [[package]] 536 | name = "windows_i686_gnu" 537 | version = "0.52.6" 538 | source = "registry+https://github.com/rust-lang/crates.io-index" 539 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 540 | 541 | [[package]] 542 | name = "windows_i686_gnullvm" 543 | version = "0.52.6" 544 | source = "registry+https://github.com/rust-lang/crates.io-index" 545 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 546 | 547 | [[package]] 548 | name = "windows_i686_msvc" 549 | version = "0.52.6" 550 | source = "registry+https://github.com/rust-lang/crates.io-index" 551 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 552 | 553 | [[package]] 554 | name = "windows_x86_64_gnu" 555 | version = "0.52.6" 556 | source = "registry+https://github.com/rust-lang/crates.io-index" 557 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 558 | 559 | [[package]] 560 | name = "windows_x86_64_gnullvm" 561 | version = "0.52.6" 562 | source = "registry+https://github.com/rust-lang/crates.io-index" 563 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 564 | 565 | [[package]] 566 | name = "windows_x86_64_msvc" 567 | version = "0.52.6" 568 | source = "registry+https://github.com/rust-lang/crates.io-index" 569 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 570 | 571 | [[package]] 572 | name = "winnow" 573 | version = "0.7.2" 574 | source = "registry+https://github.com/rust-lang/crates.io-index" 575 | checksum = "59690dea168f2198d1a3b0cac23b8063efcd11012f10ae4698f284808c8ef603" 576 | dependencies = [ 577 | "memchr", 578 | ] 579 | --------------------------------------------------------------------------------