[
  {
    "path": ".cargo/config",
    "content": "[target.x86_64-pc-windows-gnu]\nlinker = \"x86_64-w64-mingw32-gcc\"\n\n[target.i686-pc-windows-gnu]\nlinker = \"i686-w64-mingw32-gcc\"\n\n"
  },
  {
    "path": ".gitattributes",
    "content": "test/lfs.txt filter=lfs diff=lfs merge=lfs -text\n"
  },
  {
    "path": ".github/FUNDING.yml",
    "content": "# These are supported funding model platforms\n\ngithub: dalance\n"
  },
  {
    "path": ".github/dependabot.yml",
    "content": "version: 2\nupdates:\n- package-ecosystem: cargo\n  directory: \"/\"\n  schedule:\n    interval: daily\n    time: \"20:00\"\n  open-pull-requests-limit: 10\n"
  },
  {
    "path": ".github/workflows/dependabot_merge.yml",
    "content": "name: Dependabot auto-merge\non: pull_request_target\n\npermissions:\n  pull-requests: write\n  contents: write\n\njobs:\n  dependabot:\n    runs-on: ubuntu-latest\n    if: ${{ github.actor == 'dependabot[bot]' }}\n    steps:\n      - name: Dependabot metadata\n        id: metadata\n        uses: dependabot/fetch-metadata@v2.2.0\n        with:\n          github-token: '${{ secrets.GITHUB_TOKEN }}'\n      - name: Enable auto-merge for Dependabot PRs\n        if: ${{ steps.metadata.outputs.update-type == 'version-update:semver-patch' || ( !startsWith( steps.metadata.outputs.new-version, '0.' ) && steps.metadata.outputs.update-type == 'version-update:semver-minor' ) }}\n        run: gh pr merge --auto --merge \"$PR_URL\"\n        env:\n          PR_URL: ${{github.event.pull_request.html_url}}\n          GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}\n"
  },
  {
    "path": ".github/workflows/periodic.yml",
    "content": "name: Periodic\n\non:\n  schedule:\n  - cron: 0 0 * * SUN\n\njobs:\n  build:\n\n    strategy:\n      matrix:\n        os: [ubuntu-latest]\n        rust: [stable, beta, nightly]\n\n    runs-on: ${{ matrix.os }}\n\n    steps:\n    - name: Setup Rust\n      uses: hecrj/setup-rust-action@v1\n      with:\n        rust-version: ${{ matrix.rust }}\n    - name: Install ctags on Linux\n      if: matrix.os == 'ubuntu-latest'\n      run: |\n        sudo apt-get update\n        sudo apt-get install universal-ctags\n    - name: Checkout\n      uses: actions/checkout@v1\n    - name: git submodule init\n      run: |\n        git submodule init\n        git submodule update\n    - name: Run tests\n      run: cargo test -- --test-threads=1\n"
  },
  {
    "path": ".github/workflows/regression.yml",
    "content": "name: Regression\n\non:\n  push:\n    branches:\n      - master\n  pull_request:\n\njobs:\n  build:\n\n    strategy:\n      matrix:\n        os: [ubuntu-latest, macOS-latest]\n        rust: [stable]\n\n    runs-on: ${{ matrix.os }}\n\n    steps:\n    - name: Setup Rust\n      uses: hecrj/setup-rust-action@v1\n      with:\n        rust-version: ${{ matrix.rust }}\n    - name: Install ctags on Linux\n      if: matrix.os == 'ubuntu-latest'\n      run: |\n        sudo apt-get update\n        sudo apt-get install universal-ctags\n    - name: Install ctags on macOS\n      if: matrix.os == 'macOS-latest'\n      run: |\n        brew update\n        brew install universal-ctags\n        brew install git-lfs\n    - name: Checkout\n      uses: actions/checkout@v1\n    - name: git submodule init\n      run: |\n        git submodule init\n        git submodule update\n    - name: Run tests\n      run: cargo test -- --test-threads=1\n"
  },
  {
    "path": ".github/workflows/release.yml",
    "content": "name: Release\n\non:\n  workflow_dispatch:\n  push:\n    tags:\n      - 'v*.*.*'\n\njobs:\n  build:\n\n    strategy:\n      matrix:\n        os: [ubuntu-latest, macOS-latest, windows-latest]\n        rust: [stable]\n\n    runs-on: ${{ matrix.os }}\n\n    steps:\n    - name: Setup Rust\n      uses: hecrj/setup-rust-action@v1\n      with:\n        rust-version: ${{ matrix.rust }}\n    - name: Checkout\n      uses: actions/checkout@v1\n    - name: Setup MUSL\n      if: matrix.os == 'ubuntu-latest'\n      run: |\n        rustup target add x86_64-unknown-linux-musl\n        sudo apt-get -qq install musl-tools\n    - name: Setup Target\n      if: matrix.os == 'macOS-latest'\n      run: |\n        rustup target add aarch64-apple-darwin\n    - name: Build for Linux\n      if: matrix.os == 'ubuntu-latest'\n      run: make release_lnx\n    - name: Build for macOS\n      if: matrix.os == 'macOS-latest'\n      run: make release_mac\n    - name: Build for Windows\n      if: matrix.os == 'windows-latest'\n      run: make release_win\n    - name: Upload artifacts\n      uses: actions/upload-artifact@v3\n      with:\n        name: ptags\n        path: '*.zip'\n    - name: Release\n      if: github.event_name == 'push' && github.ref_type == 'tag'\n      uses: softprops/action-gh-release@v1\n      with:\n        files: '*.zip'\n      env:\n        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n"
  },
  {
    "path": ".gitignore",
    "content": "# Generated by Cargo\n# will have compiled files and executables\n/target/\n\n# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries\n# More information here http://doc.crates.io/guide.html#cargotoml-vs-cargolock\n#Cargo.lock\n\n# These are backup files generated by rustfmt\n**/*.rs.bk\n\ndata/\ntags\n*.zip\n*.gz\n"
  },
  {
    "path": ".gitmodules",
    "content": "[submodule \"test/ptags_test\"]\n\tpath = test/ptags_test\n\turl = https://github.com/dalance/ptags_test.git\n"
  },
  {
    "path": "Cargo.toml",
    "content": "[package]\nname = \"ptags\"\nversion = \"0.3.5\"\nauthors = [\"dalance@gmail.com\"]\nrepository = \"https://github.com/dalance/ptags\"\nkeywords = [\"ctags\", \"universal-ctags\"]\ncategories = [\"command-line-utilities\", \"development-tools\"]\nlicense = \"MIT\"\nreadme = \"README.md\"\ndescription = \"A parallel universal-ctags wrapper for git repository\"\nedition = \"2018\"\n\n[badges]\ntravis-ci = { repository = \"dalance/ptags\" }\nappveyor  = { repository = \"dalance/ptags\", branch = \"master\", service = \"github\" }\ncodecov   = { repository = \"dalance/ptags\", branch = \"master\", service = \"github\" }\n\n[dependencies]\nanyhow         = \"1.0\"\ndirs           = \"6\"\nnix            = { version = \"0.31.3\", features = [\"fs\"] }\nserde          = \"1\"\nserde_derive   = \"1\"\nstructopt      = \"0.3\"\nstructopt-toml = \"0.5\"\ntempfile       = \"3\"\nthiserror      = \"2.0\"\ntoml           = \"1.1\"\n\n[dev-dependencies]\nbencher = \"0.1\"\n\n[lib]\nname = \"ptagslib\"\npath = \"src/lib.rs\"\n\n[[bin]]\nname = \"ptags\"\npath = \"src/main.rs\"\n\n[[bench]]\nname    = \"ptags_bench\"\nharness = false\n\n[package.metadata.release]\npre-release-commit-message  = \"Prepare to v{{version}}\"\npost-release-commit-message = \"Start next development iteration v{{version}}\"\ntag-message                 = \"Bump version to {{version}}\"\ntag-prefix                  = \"\"\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2018 dalance <dalance@gmail.com>\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "Makefile",
    "content": "VERSION = $(patsubst \"%\",%, $(word 3, $(shell grep version Cargo.toml)))\nBUILD_TIME = $(shell date +\"%Y/%m/%d %H:%M:%S\")\nGIT_REVISION = $(shell git log -1 --format=\"%h\")\nRUST_VERSION = $(word 2, $(shell rustc -V))\nLONG_VERSION = \"$(VERSION) ( rev: $(GIT_REVISION), rustc: $(RUST_VERSION), build at: $(BUILD_TIME) )\"\nBIN_NAME = ptags\n\nexport LONG_VERSION\n\n.PHONY: all test clean release_lnx release_win release_mac\n\nall: test\n\ntest:\n\tcargo test -- --test-threads=1\n\nwatch:\n\tcargo watch \"test -- --test-threads=1\"\n\nclean:\n\tcargo clean\n\nrelease_lnx:\n\tcargo build --release --target=x86_64-unknown-linux-musl\n\tzip -j ${BIN_NAME}-v${VERSION}-x86_64-lnx.zip target/x86_64-unknown-linux-musl/release/${BIN_NAME}\n\nrelease_win:\n\tcargo build --release --target=x86_64-pc-windows-msvc\n\t7z a ${BIN_NAME}-v${VERSION}-x86_64-win.zip target/x86_64-pc-windows-msvc/release/${BIN_NAME}.exe\n\nrelease_mac:\n\tcargo build --release --target=x86_64-apple-darwin\n\tzip -j ${BIN_NAME}-v${VERSION}-x86_64-mac.zip target/x86_64-apple-darwin/release/${BIN_NAME}\n\tcargo build --release --target=aarch64-apple-darwin\n\tzip -j ${BIN_NAME}-v${VERSION}-aarch64-mac.zip target/aarch64-apple-darwin/release/${BIN_NAME}\n"
  },
  {
    "path": "README.md",
    "content": "# ptags\nA parallel [universal-ctags](https://ctags.io) wrapper for git repository\n\n[![Actions Status](https://github.com/dalance/ptags/workflows/Regression/badge.svg)](https://github.com/dalance/ptags/actions)\n[![Crates.io](https://img.shields.io/crates/v/ptags.svg)](https://crates.io/crates/ptags)\n[![codecov](https://codecov.io/gh/dalance/ptags/branch/master/graph/badge.svg)](https://codecov.io/gh/dalance/ptags)\n\n## Description\n\n**ptags** is a [universal-ctags](https://ctags.io) wrapper to have the following features.\n- Search git tracked files only ( `.gitignore` support )\n- Call `ctags` command in parallel for acceleration\n    - Up to x5 faster than universal-ctags\n\n## Install\n\n### Download binary\n\nDownload from [release page](https://github.com/dalance/ptags/releases/latest), and extract to the directory in PATH.\n\n### Arch Linux\n\nYou can install from AUR.\n\n- https://aur.archlinux.org/packages/ptags/\n- https://aur.archlinux.org/packages/ptags-git/\n\nIf you use `yay`, you can install like below:\n\n```\nyay -S ptags       // latest tagged version\nyay -S ptags-git   // current master of git repo\n```\n\n### Cargo\n\nYou can install by [cargo](https://crates.io).\n\n```\ncargo install ptags\n```\n\n## Requirement\n\n**ptags** uses `ctags` and `git` command internally.\nThe tested version is below.\n\n| Command   | Version                                               |\n| --------- | ----------------------------------------------------- |\n| `ctags`   | Universal Ctags 0.0.0(f9e6e3c1) / Exuberant Ctags 5.8 |\n| `git`     | git version 2.14.2                                    |\n| `git-lfs` | git-lfs/2.3.3                                         |\n\n## Usage\n\n```\nptags 0.1.12-pre\ndalance@gmail.com\nA parallel universal-ctags wrapper for git repository\n\nUSAGE:\n    ptags [FLAGS] [OPTIONS] [--] [DIR]\n\nFLAGS:\n        --config               Generate configuration sample file\n        --exclude-lfs          Exclude git-lfs tracked files\n    -h, --help                 Prints help information\n        --include-ignored      Include ignored files\n        --include-submodule    Include submodule files\n        --include-untracked    Include untracked files\n    -s, --stat                 Show statistics\n        --unsorted             Disable tags sort\n        --validate-utf8        Validate UTF8 sequence of tag file\n    -V, --version              Prints version information\n    -v, --verbose              Verbose mode\n\nOPTIONS:\n        --bin-ctags <bin_ctags>           Path to ctags binary [default: ctags]\n        --bin-git <bin_git>               Path to git binary [default: git]\n        --completion <completion>         Generate shell completion file [possible values: bash, fish,\n                                          zsh, powershell]\n    -e, --exclude <exclude>...            Glob pattern of exclude file ( ex. --exclude '*.rs' )\n    -c, --opt-ctags <opt_ctags>...        Options passed to ctags\n    -g, --opt-git <opt_git>...            Options passed to git\n        --opt-git-lfs <opt_git_lfs>...    Options passed to git-lfs\n    -f, --file <output>                   Output filename ( filename '-' means output to stdout ) [default: tags]\n    -t, --thread <thread>                 Number of threads [default: 8]\n\nARGS:\n    <DIR>    Search directory [default: .]\n```\n\nYou can pass options to `ctags` by`-c`/`--ctags_opt` option like below.\n\n```\nptags -c --links=no -c --languages=Rust\n```\n\nSearched file types per options are below.\n`--include-submodule` and `--include_untracked` are exclusive.\nThis is the restriction of `git ls-files`.\nAny include/exclude options without the above combination can be used simultaneously.\n\n| File type     | Default  | --exclude-lfs | --include-ignored | --include-submodule | --include-untracked |\n| ------------- | -------- | ------------- | ----------------- | ------------------- | ------------------- |\n| tracked       | o        | o             | o                 | o                   | o                   |\n| untracked     | x        | x             | x                 | x                   | o                   |\n| ignored       | x        | x             | o                 | x                   | x                   |\n| lfs tracked   | o        | x             | o                 | o                   | o                   |\n| in submodules | x        | x             | x                 | o                   | x                   |\n\nYou can override any default option by `~/.ptags.toml` like below.\nThe complete example of `~/.ptags.toml` can be generated by `--config` option.\n\n```toml\nthread = 16\nbin_ctags = \"ctags2\"\nbin_git = \"git2\"\n```\n\n## Benchmark\n\n### Environment\n- CPU: Ryzen Threadripper 1950X\n- MEM: 128GB\n- OS : CentOS 7.4.1708\n\n### Data\n\n| Name    | Repository                           | Revision     | Files  | Size[GB] |\n| ------- | ------------------------------------ | ------------ | ------ | -------- |\n| source0 | https://github.com/neovim/neovim     | f5b0f5e17    | 2370   | 0.1      |\n| source1 | https://github.com/llvm-mirror/llvm  | ddf9edb4020  | 29670  | 1.2      |\n| source2 | https://github.com/torvalds/linux    | 071e31e254e0 | 52998  | 2.2      |\n| source3 | https://github.com/chromium/chromium | d79c68510b7e | 293205 | 13       |\n\n### Result\n\n**ptags** is up to x5 faster than universal-ctags.\n\n| Command       | Version                         | source0         | source1         | source2          | source3         |\n| ------------- | ------------------------------- | --------------- | --------------- | ---------------- | --------------- |\n| `ctags -R`    | Universal Ctags 0.0.0(f9e6e3c1) | 0.41s ( x1 )    | 3.42s ( x1 )    | 23.64s ( x1 )    | 32.23 ( x1 )    |\n| `ptags -t 16` | ptags 0.1.4                     | 0.13s ( x3.15 ) | 0.58s ( x5.90 ) | 4.24s  ( x5.58 ) | 7.27s ( x4.43 ) |\n\n"
  },
  {
    "path": "benches/ptags_bench.rs",
    "content": "#[macro_use]\nextern crate bencher;\nextern crate ptagslib;\nextern crate structopt;\n\nuse bencher::Bencher;\nuse ptagslib::bin::{run_opt, Opt};\nuse structopt::StructOpt;\n\nfn bench_default(bench: &mut Bencher) {\n    bench.iter(|| {\n        let args = vec![\"ptags\"];\n        let opt = Opt::from_iter(args.iter());\n        let _ = run_opt(&opt);\n    })\n}\n\nfn bench_unsorted(bench: &mut Bencher) {\n    bench.iter(|| {\n        let args = vec![\"ptags\", \"--unsorted\"];\n        let opt = Opt::from_iter(args.iter());\n        let _ = run_opt(&opt);\n    })\n}\n\nbenchmark_group!(benches, bench_default, bench_unsorted);\nbenchmark_main!(benches);\n"
  },
  {
    "path": "src/bin.rs",
    "content": "use crate::cmd_ctags::CmdCtags;\nuse crate::cmd_git::CmdGit;\nuse anyhow::{Context, Error};\nuse dirs;\nuse serde_derive::{Deserialize, Serialize};\nuse std::fs;\nuse std::io::BufRead;\nuse std::io::{stdout, BufWriter, Read, Write};\nuse std::path::PathBuf;\nuse std::process::Output;\nuse std::str;\nuse std::time::{Duration, Instant};\nuse structopt::{clap, StructOpt};\nuse structopt_toml::StructOptToml;\nuse toml;\n\n// ---------------------------------------------------------------------------------------------------------------------\n// Options\n// ---------------------------------------------------------------------------------------------------------------------\n\n#[derive(Debug, Deserialize, Serialize, StructOpt, StructOptToml)]\n#[serde(default)]\n#[structopt(name = \"ptags\")]\n#[structopt(long_version = option_env!(\"LONG_VERSION\").unwrap_or(env!(\"CARGO_PKG_VERSION\")))]\n#[structopt(setting = clap::AppSettings::AllowLeadingHyphen)]\n#[structopt(setting = clap::AppSettings::ColoredHelp)]\npub struct Opt {\n    /// Number of threads\n    #[structopt(short = \"t\", long = \"thread\", default_value = \"8\")]\n    pub thread: usize,\n\n    /// Output filename ( filename '-' means output to stdout )\n    #[structopt(short = \"f\", long = \"file\", default_value = \"tags\", parse(from_os_str))]\n    pub output: PathBuf,\n\n    /// Search directory\n    #[structopt(name = \"DIR\", default_value = \".\", parse(from_os_str))]\n    pub dir: PathBuf,\n\n    /// Show statistics\n    #[structopt(short = \"s\", long = \"stat\")]\n    pub stat: bool,\n\n    /// Filename of input file list\n    #[structopt(short = \"L\", long = \"list\")]\n    pub list: Option<String>,\n\n    /// Path to ctags binary\n    #[structopt(long = \"bin-ctags\", default_value = \"ctags\", parse(from_os_str))]\n    pub bin_ctags: PathBuf,\n\n    /// Path to git binary\n    #[structopt(long = \"bin-git\", default_value = \"git\", parse(from_os_str))]\n    pub bin_git: PathBuf,\n\n    /// Options passed to ctags\n    #[structopt(short = \"c\", long = \"opt-ctags\", number_of_values = 1)]\n    pub opt_ctags: Vec<String>,\n\n    /// Options passed to git\n    #[structopt(short = \"g\", long = \"opt-git\", number_of_values = 1)]\n    pub opt_git: Vec<String>,\n\n    /// Options passed to git-lfs\n    #[structopt(long = \"opt-git-lfs\", number_of_values = 1)]\n    pub opt_git_lfs: Vec<String>,\n\n    /// Verbose mode\n    #[structopt(short = \"v\", long = \"verbose\")]\n    pub verbose: bool,\n\n    /// Exclude git-lfs tracked files\n    #[structopt(long = \"exclude-lfs\")]\n    pub exclude_lfs: bool,\n\n    /// Include untracked files\n    #[structopt(long = \"include-untracked\")]\n    pub include_untracked: bool,\n\n    /// Include ignored files\n    #[structopt(long = \"include-ignored\")]\n    pub include_ignored: bool,\n\n    /// Include submodule files\n    #[structopt(long = \"include-submodule\")]\n    pub include_submodule: bool,\n\n    /// Validate UTF8 sequence of tag file\n    #[structopt(long = \"validate-utf8\")]\n    pub validate_utf8: bool,\n\n    /// Disable tags sort\n    #[structopt(long = \"unsorted\")]\n    pub unsorted: bool,\n\n    /// Glob pattern of exclude file ( ex. --exclude '*.rs' )\n    #[structopt(short = \"e\", long = \"exclude\", number_of_values = 1)]\n    pub exclude: Vec<String>,\n\n    /// Generate shell completion file\n    #[structopt(\n        long = \"completion\",\n        possible_values = &[\"bash\", \"fish\", \"zsh\", \"powershell\"]\n    )]\n    pub completion: Option<String>,\n\n    /// Generate configuration sample file\n    #[structopt(long = \"config\")]\n    pub config: bool,\n}\n\n// ---------------------------------------------------------------------------------------------------------------------\n// Functions\n// ---------------------------------------------------------------------------------------------------------------------\n\nmacro_rules! watch_time (\n    ( $func:block ) => (\n        {\n            let beg = Instant::now();\n            $func;\n            Instant::now() - beg\n        }\n    );\n);\n\npub fn git_files(opt: &Opt) -> Result<Vec<String>, Error> {\n    let list = CmdGit::get_files(&opt)?;\n    let mut files = vec![String::from(\"\"); opt.thread];\n\n    for (i, f) in list.iter().enumerate() {\n        files[i % opt.thread].push_str(f);\n        files[i % opt.thread].push_str(\"\\n\");\n    }\n\n    Ok(files)\n}\n\npub fn input_files(file: &String, opt: &Opt) -> Result<Vec<String>, Error> {\n    let mut list = Vec::new();\n    if file == &String::from(\"-\") {\n        let stdin = std::io::stdin();\n        for line in stdin.lock().lines() {\n            list.push(String::from(line?));\n        }\n    } else {\n        for line in fs::read_to_string(file)?.lines() {\n            list.push(String::from(line));\n        }\n    }\n\n    let mut files = vec![String::from(\"\"); opt.thread];\n\n    for (i, f) in list.iter().enumerate() {\n        files[i % opt.thread].push_str(f);\n        files[i % opt.thread].push_str(\"\\n\");\n    }\n\n    Ok(files)\n}\n\nfn call_ctags(opt: &Opt, files: &[String]) -> Result<Vec<Output>, Error> {\n    Ok(CmdCtags::call(&opt, &files)?)\n}\n\nfn get_tags_header(opt: &Opt) -> Result<String, Error> {\n    Ok(CmdCtags::get_tags_header(&opt).context(\"failed to get ctags header\")?)\n}\n\nfn write_tags(opt: &Opt, outputs: &[Output]) -> Result<(), Error> {\n    let mut iters = Vec::new();\n    let mut lines = Vec::new();\n    for o in outputs {\n        let mut iter = if opt.validate_utf8 {\n            str::from_utf8(&o.stdout)?.lines()\n        } else {\n            unsafe { str::from_utf8_unchecked(&o.stdout).lines() }\n        };\n        lines.push(iter.next());\n        iters.push(iter);\n    }\n\n    let mut f = if opt.output.to_str().unwrap_or(\"\") == \"-\" {\n        BufWriter::new(Box::new(stdout()) as Box<dyn Write>)\n    } else {\n        let f = fs::File::create(&opt.output)?;\n        BufWriter::new(Box::new(f) as Box<dyn Write>)\n    };\n\n    f.write(get_tags_header(&opt)?.as_bytes())?;\n\n    while lines.iter().any(|x| x.is_some()) {\n        let mut min = 0;\n        for i in 1..lines.len() {\n            if opt.unsorted {\n                if !lines[i].is_none() && lines[min].is_none() {\n                    min = i;\n                }\n            } else {\n                if !lines[i].is_none()\n                    && (lines[min].is_none() || lines[i].unwrap() < lines[min].unwrap())\n                {\n                    min = i;\n                }\n            }\n        }\n        f.write(lines[min].unwrap().as_bytes())?;\n        f.write(\"\\n\".as_bytes())?;\n        lines[min] = iters[min].next();\n    }\n\n    Ok(())\n}\n\n// ---------------------------------------------------------------------------------------------------------------------\n// Run\n// ---------------------------------------------------------------------------------------------------------------------\n\npub fn run_opt(opt: &Opt) -> Result<(), Error> {\n    if opt.config {\n        let toml = toml::to_string(&opt)?;\n        println!(\"{}\", toml);\n        return Ok(());\n    }\n\n    match opt.completion {\n        Some(ref x) => {\n            let shell = match x.as_str() {\n                \"bash\" => clap::Shell::Bash,\n                \"fish\" => clap::Shell::Fish,\n                \"zsh\" => clap::Shell::Zsh,\n                \"powershell\" => clap::Shell::PowerShell,\n                _ => clap::Shell::Bash,\n            };\n            Opt::clap().gen_completions(\"ptags\", shell, \"./\");\n            return Ok(());\n        }\n        None => {}\n    }\n\n    let files;\n    let time_git_files;\n    if let Some(ref list) = opt.list {\n        files = input_files(list, &opt).context(\"failed to get file list\")?;\n        time_git_files = Duration::from_secs(0);\n    } else {\n        time_git_files = watch_time!({\n            files = git_files(&opt).context(\"failed to get file list\")?;\n        });\n    }\n\n    let outputs;\n    let time_call_ctags = watch_time!({\n        outputs = call_ctags(&opt, &files).context(\"failed to call ctags\")?;\n    });\n\n    let time_write_tags = watch_time!({\n        let _ = write_tags(&opt, &outputs)\n            .context(format!(\"failed to write file ({:?})\", &opt.output))?;\n    });\n\n    if opt.stat {\n        let sum: usize = files.iter().map(|x| x.lines().count()).sum();\n\n        eprintln!(\"\\nStatistics\");\n        eprintln!(\"- Options\");\n        eprintln!(\"    thread    : {}\\n\", opt.thread);\n\n        eprintln!(\"- Searched files\");\n        eprintln!(\"    total     : {}\\n\", sum);\n\n        eprintln!(\"- Elapsed time[ms]\");\n        eprintln!(\"    git_files : {}\", time_git_files.as_millis());\n        eprintln!(\"    call_ctags: {}\", time_call_ctags.as_millis());\n        eprintln!(\"    write_tags: {}\", time_write_tags.as_millis());\n    }\n\n    Ok(())\n}\n\npub fn run() -> Result<(), Error> {\n    let cfg_path = match dirs::home_dir() {\n        Some(mut path) => {\n            path.push(\".ptags.toml\");\n            if path.exists() {\n                Some(path)\n            } else {\n                None\n            }\n        }\n        None => None,\n    };\n\n    let opt = match cfg_path {\n        Some(path) => {\n            let mut f =\n                fs::File::open(&path).context(format!(\"failed to open file ({:?})\", path))?;\n            let mut s = String::new();\n            let _ = f.read_to_string(&mut s);\n            Opt::from_args_with_toml(&s).context(format!(\"failed to parse toml ({:?})\", path))?\n        }\n        None => Opt::from_args(),\n    };\n    run_opt(&opt)\n}\n\n// ---------------------------------------------------------------------------------------------------------------------\n// Test\n// ---------------------------------------------------------------------------------------------------------------------\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n    use std::path::Path;\n\n    #[test]\n    fn test_run() {\n        let args = vec![\"ptags\"];\n        let opt = Opt::from_iter(args.iter());\n        let ret = run_opt(&opt);\n        assert!(ret.is_ok());\n    }\n\n    #[test]\n    fn test_run_opt() {\n        let args = vec![\"ptags\", \"-s\", \"-v\", \"--validate-utf8\", \"--unsorted\"];\n        let opt = Opt::from_iter(args.iter());\n        let ret = run_opt(&opt);\n        assert!(ret.is_ok());\n    }\n\n    #[test]\n    fn test_run_fail() {\n        let args = vec![\"ptags\", \"--bin-git\", \"aaa\"];\n        let opt = Opt::from_iter(args.iter());\n        let ret = run_opt(&opt);\n        assert_eq!(\n            &format!(\"{:?}\", ret)[0..42],\n            \"Err(failed to get file list\\n\\nCaused by:\\n  \"\n        );\n    }\n\n    #[test]\n    fn test_run_completion() {\n        let args = vec![\"ptags\", \"--completion\", \"bash\"];\n        let opt = Opt::from_iter(args.iter());\n        let ret = run_opt(&opt);\n        assert!(ret.is_ok());\n        let args = vec![\"ptags\", \"--completion\", \"fish\"];\n        let opt = Opt::from_iter(args.iter());\n        let ret = run_opt(&opt);\n        assert!(ret.is_ok());\n        let args = vec![\"ptags\", \"--completion\", \"zsh\"];\n        let opt = Opt::from_iter(args.iter());\n        let ret = run_opt(&opt);\n        assert!(ret.is_ok());\n        let args = vec![\"ptags\", \"--completion\", \"powershell\"];\n        let opt = Opt::from_iter(args.iter());\n        let ret = run_opt(&opt);\n        assert!(ret.is_ok());\n\n        assert!(Path::new(\"ptags.bash\").exists());\n        assert!(Path::new(\"ptags.fish\").exists());\n        assert!(Path::new(\"_ptags\").exists());\n        assert!(Path::new(\"_ptags.ps1\").exists());\n        let _ = fs::remove_file(\"ptags.bash\");\n        let _ = fs::remove_file(\"ptags.fish\");\n        let _ = fs::remove_file(\"_ptags\");\n        let _ = fs::remove_file(\"_ptags.ps1\");\n    }\n\n    #[test]\n    fn test_run_config() {\n        let args = vec![\"ptags\", \"--config\"];\n        let opt = Opt::from_iter(args.iter());\n        let ret = run_opt(&opt);\n        assert!(ret.is_ok());\n    }\n}\n"
  },
  {
    "path": "src/cmd_ctags.rs",
    "content": "use crate::bin::Opt;\nuse anyhow::{bail, Context, Error};\n#[cfg(target_os = \"linux\")]\nuse nix::fcntl::{fcntl, FcntlArg};\nuse std::fs;\nuse std::fs::File;\nuse std::io::{BufReader, Read, Write};\nuse std::path::PathBuf;\nuse std::process::{ChildStdin, Command, Output, Stdio};\nuse std::str;\nuse std::sync::mpsc;\nuse std::thread;\nuse tempfile::NamedTempFile;\nuse thiserror::Error;\n\n// ---------------------------------------------------------------------------------------------------------------------\n// Error\n// ---------------------------------------------------------------------------------------------------------------------\n\n#[derive(Debug, Error)]\nenum CtagsError {\n    #[error(\"failed to execute ctags command ({})\\n{}\", cmd, err)]\n    ExecFailed { cmd: String, err: String },\n\n    #[error(\"failed to call ctags command ({})\", cmd)]\n    CallFailed { cmd: String },\n\n    #[error(\"failed to convert to UTF-8 ({:?})\", s)]\n    ConvFailed { s: Vec<u8> },\n}\n\n// ---------------------------------------------------------------------------------------------------------------------\n// CmdCtags\n// ---------------------------------------------------------------------------------------------------------------------\n\npub struct CmdCtags;\n\nimpl CmdCtags {\n    pub fn call(opt: &Opt, files: &[String]) -> Result<Vec<Output>, Error> {\n        let mut args = Vec::new();\n        args.push(String::from(\"-L -\"));\n        args.push(String::from(\"-f -\"));\n        if opt.unsorted {\n            args.push(String::from(\"--sort=no\"));\n        }\n        for e in &opt.exclude {\n            args.push(String::from(format!(\"--exclude={}\", e)));\n        }\n        args.append(&mut opt.opt_ctags.clone());\n\n        let cmd = CmdCtags::get_cmd(&opt, &args);\n\n        let (tx, rx) = mpsc::channel::<Result<Output, Error>>();\n\n        for i in 0..opt.thread {\n            let tx = tx.clone();\n            let file = files[i].clone();\n            let dir = opt.dir.clone();\n            let bin_ctags = opt.bin_ctags.clone();\n            let args = args.clone();\n            let cmd = cmd.clone();\n\n            if opt.verbose {\n                eprintln!(\"Call : {}\", cmd);\n            }\n\n            thread::spawn(move || {\n                let child = Command::new(bin_ctags.clone())\n                    .args(args)\n                    .current_dir(dir)\n                    .stdin(Stdio::piped())\n                    .stdout(Stdio::piped())\n                    //.stderr(Stdio::piped()) // Stdio::piped is x2 slow to wait_with_output() completion\n                    .stderr(Stdio::null())\n                    .spawn();\n                match child {\n                    Ok(mut x) => {\n                        {\n                            let stdin = x.stdin.as_mut().unwrap();\n                            let pipe_size = std::cmp::min(file.len() as i32, 1048576);\n                            let _ = CmdCtags::set_pipe_size(&stdin, pipe_size)\n                                .or_else(|x| tx.send(Err(x.into())));\n                            let _ = stdin.write_all(file.as_bytes());\n                        }\n                        match x.wait_with_output() {\n                            Ok(x) => {\n                                let _ = tx.send(Ok(x));\n                            }\n                            Err(x) => {\n                                let _ = tx.send(Err(x.into()));\n                            }\n                        }\n                    }\n                    Err(_) => {\n                        let _ = tx.send(Err(CtagsError::CallFailed { cmd }.into()));\n                    }\n                }\n            });\n        }\n\n        let mut children = Vec::new();\n        for _ in 0..opt.thread {\n            children.push(rx.recv());\n        }\n\n        let mut outputs = Vec::new();\n        for child in children {\n            let output = child??;\n\n            if !output.status.success() {\n                bail!(CtagsError::ExecFailed {\n                    cmd: cmd,\n                    err: String::from(str::from_utf8(&output.stderr).context(\n                        CtagsError::ConvFailed {\n                            s: output.stderr.to_vec(),\n                        }\n                    )?)\n                });\n            }\n\n            outputs.push(output);\n        }\n\n        Ok(outputs)\n    }\n\n    pub fn get_tags_header(opt: &Opt) -> Result<String, Error> {\n        let tmp_empty = NamedTempFile::new()?;\n        let tmp_tags = NamedTempFile::new()?;\n        let tmp_tags_path: PathBuf = tmp_tags.path().into();\n        // In windiws environment, write access by ctags to the opened tmp_tags fails.\n        // So the tmp_tags must be closed and deleted.\n        tmp_tags.close()?;\n\n        let _ = Command::new(&opt.bin_ctags)\n            .arg(format!(\"-L {}\", tmp_empty.path().to_string_lossy()))\n            .arg(format!(\"-f {}\", tmp_tags_path.to_string_lossy()))\n            .args(&opt.opt_ctags)\n            .current_dir(&opt.dir)\n            .status();\n        let mut f = BufReader::new(File::open(&tmp_tags_path)?);\n        let mut s = String::new();\n        f.read_to_string(&mut s)?;\n\n        fs::remove_file(&tmp_tags_path)?;\n\n        Ok(s)\n    }\n\n    fn get_cmd(opt: &Opt, args: &[String]) -> String {\n        let mut cmd = format!(\n            \"cd {}; {}\",\n            opt.dir.to_string_lossy(),\n            opt.bin_ctags.to_string_lossy()\n        );\n        for arg in args {\n            cmd = format!(\"{} {}\", cmd, arg);\n        }\n        cmd\n    }\n\n    #[allow(dead_code)]\n    fn is_exuberant_ctags(opt: &Opt) -> Result<bool, Error> {\n        let output = Command::new(&opt.bin_ctags)\n            .arg(\"--version\")\n            .current_dir(&opt.dir)\n            .output()?;\n        Ok(str::from_utf8(&output.stdout)?.starts_with(\"Exuberant Ctags\"))\n    }\n\n    #[cfg(target_os = \"linux\")]\n    fn set_pipe_size(stdin: &ChildStdin, len: i32) -> Result<(), Error> {\n        fcntl(stdin, FcntlArg::F_SETPIPE_SZ(len))?;\n        Ok(())\n    }\n\n    #[cfg(not(target_os = \"linux\"))]\n    fn set_pipe_size(_stdin: &ChildStdin, _len: i32) -> Result<(), Error> {\n        Ok(())\n    }\n}\n\n// ---------------------------------------------------------------------------------------------------------------------\n// Test\n// ---------------------------------------------------------------------------------------------------------------------\n\n#[cfg(test)]\nmod tests {\n    use super::super::bin::{git_files, Opt};\n    use super::CmdCtags;\n    use std::str;\n    use structopt::StructOpt;\n\n    #[test]\n    fn test_call() {\n        let args = vec![\"ptags\", \"-t\", \"1\", \"--exclude=README.md\"];\n        let opt = Opt::from_iter(args.iter());\n        let files = git_files(&opt).unwrap();\n        let outputs = CmdCtags::call(&opt, &files).unwrap();\n        let mut iter = str::from_utf8(&outputs[0].stdout).unwrap().lines();\n        assert_eq!(\n            iter.next().unwrap_or(\"\"),\n            \"BIN_NAME\\tMakefile\\t/^BIN_NAME = ptags$/;\\\"\\tm\"\n        );\n    }\n\n    #[test]\n    fn test_call_with_opt() {\n        let args = vec![\"ptags\", \"-t\", \"1\", \"--opt-ctags=-u\"];\n        let opt = Opt::from_iter(args.iter());\n        let files = git_files(&opt).unwrap();\n        let outputs = CmdCtags::call(&opt, &files).unwrap();\n        let mut iter = str::from_utf8(&outputs[0].stdout).unwrap().lines();\n        assert_eq!(\n                iter.next().unwrap_or(\"\"),\n                \"VERSION\\tMakefile\\t/^VERSION = $(patsubst \\\"%\\\",%, $(word 3, $(shell grep version Cargo.toml)))$/;\\\"\\tm\"\n            );\n    }\n\n    #[test]\n    fn test_call_exclude() {\n        let args = vec![\n            \"ptags\",\n            \"-t\",\n            \"1\",\n            \"--exclude=Make*\",\n            \"--exclude=README.md\",\n            \"-v\",\n        ];\n        let opt = Opt::from_iter(args.iter());\n        let files = git_files(&opt).unwrap();\n        let outputs = CmdCtags::call(&opt, &files).unwrap();\n        let mut iter = str::from_utf8(&outputs[0].stdout).unwrap().lines();\n\n        // Exuberant Ctags doesn't support Rust ( *.rs ).\n        // So the result becomes empty when 'Makefile' is excluded.\n        if CmdCtags::is_exuberant_ctags(&opt).unwrap() {\n            assert_eq!(iter.next().unwrap_or(\"\"), \"\");\n        } else {\n            assert_eq!(\n                iter.next().unwrap_or(\"\"),\n                \"CallFailed\\tsrc/cmd_ctags.rs\\t/^    CallFailed { cmd: String },$/;\\\"\\te\\tenum:CtagsError\"\n            );\n        }\n    }\n\n    #[test]\n    fn test_command_fail() {\n        let args = vec![\"ptags\", \"--bin-ctags\", \"aaa\"];\n        let opt = Opt::from_iter(args.iter());\n        let files = git_files(&opt).unwrap();\n        let outputs = CmdCtags::call(&opt, &files);\n        assert_eq!(\n            &format!(\"{:?}\", outputs),\n            \"Err(failed to call ctags command (cd .; aaa -L - -f -))\"\n        );\n    }\n\n    #[test]\n    fn test_ctags_fail() {\n        let args = vec![\"ptags\", \"--opt-ctags=--u\"];\n        let opt = Opt::from_iter(args.iter());\n        let files = git_files(&opt).unwrap();\n        let outputs = CmdCtags::call(&opt, &files);\n        assert_eq!(\n            &format!(\"{:?}\", outputs)[0..60],\n            \"Err(failed to execute ctags command (cd .; ctags -L - -f - -\"\n        );\n    }\n\n    #[test]\n    fn test_get_tags_header() {\n        let args = vec![\"ptags\"];\n        let opt = Opt::from_iter(args.iter());\n        let output = CmdCtags::get_tags_header(&opt).unwrap();\n        let output = output.lines().next();\n        assert_eq!(&output.unwrap_or(\"\")[0..5], \"!_TAG\");\n    }\n}\n"
  },
  {
    "path": "src/cmd_git.rs",
    "content": "use crate::bin::Opt;\nuse anyhow::{bail, Context, Error};\nuse std::process::{Command, Output};\nuse std::str;\nuse thiserror::Error;\n\n// ---------------------------------------------------------------------------------------------------------------------\n// Error\n// ---------------------------------------------------------------------------------------------------------------------\n\n#[derive(Debug, Error)]\nenum GitError {\n    #[error(\"failed to execute git command ({})\\n{}\", cmd, err)]\n    ExecFailed { cmd: String, err: String },\n\n    #[error(\"failed to call git command ({})\", cmd)]\n    CallFailed { cmd: String },\n\n    #[error(\"failed to convert to UTF-8 ({:?})\", s)]\n    ConvFailed { s: Vec<u8> },\n}\n\n// ---------------------------------------------------------------------------------------------------------------------\n// CmdGit\n// ---------------------------------------------------------------------------------------------------------------------\n\npub struct CmdGit;\n\nimpl CmdGit {\n    pub fn get_files(opt: &Opt) -> Result<Vec<String>, Error> {\n        let mut list = CmdGit::ls_files(&opt)?;\n        if opt.exclude_lfs {\n            let lfs_list = CmdGit::lfs_ls_files(&opt)?;\n            let mut new_list = Vec::new();\n            for l in list {\n                if !lfs_list.contains(&l) {\n                    new_list.push(l);\n                }\n            }\n            list = new_list;\n        }\n        Ok(list)\n    }\n\n    fn call(opt: &Opt, args: &[String]) -> Result<Output, Error> {\n        let cmd = CmdGit::get_cmd(&opt, &args);\n        if opt.verbose {\n            eprintln!(\"Call : {}\", cmd);\n        }\n\n        let output = Command::new(&opt.bin_git)\n            .args(args)\n            .current_dir(&opt.dir)\n            .output()\n            .context(GitError::CallFailed { cmd: cmd.clone() })?;\n\n        if !output.status.success() {\n            bail!(GitError::ExecFailed {\n                cmd: cmd,\n                err: String::from(str::from_utf8(&output.stderr).context(\n                    GitError::ConvFailed {\n                        s: output.stderr.to_vec(),\n                    }\n                )?)\n            });\n        }\n\n        Ok(output)\n    }\n\n    fn ls_files(opt: &Opt) -> Result<Vec<String>, Error> {\n        let mut args = vec![String::from(\"ls-files\")];\n        args.push(String::from(\"--cached\"));\n        args.push(String::from(\"--exclude-standard\"));\n        if opt.include_submodule {\n            args.push(String::from(\"--recurse-submodules\"));\n        } else if opt.include_untracked {\n            args.push(String::from(\"--other\"));\n        } else if opt.include_ignored {\n            args.push(String::from(\"--ignored\"));\n            args.push(String::from(\"--other\"));\n        }\n        args.append(&mut opt.opt_git.clone());\n\n        let output = CmdGit::call(&opt, &args)?;\n\n        let list = str::from_utf8(&output.stdout)\n            .context(GitError::ConvFailed {\n                s: output.stdout.to_vec(),\n            })?\n            .lines();\n        let mut ret = Vec::new();\n        for l in list {\n            ret.push(String::from(l));\n        }\n        ret.sort();\n\n        if opt.verbose {\n            eprintln!(\"Files: {}\", ret.len());\n        }\n\n        Ok(ret)\n    }\n\n    fn lfs_ls_files(opt: &Opt) -> Result<Vec<String>, Error> {\n        let mut args = vec![String::from(\"lfs\"), String::from(\"ls-files\")];\n        args.append(&mut opt.opt_git_lfs.clone());\n\n        let output = CmdGit::call(&opt, &args)?;\n\n        let cdup = CmdGit::show_cdup(&opt)?;\n        let prefix = CmdGit::show_prefix(&opt)?;\n\n        let list = str::from_utf8(&output.stdout)\n            .context(GitError::ConvFailed {\n                s: output.stdout.to_vec(),\n            })?\n            .lines();\n        let mut ret = Vec::new();\n        for l in list {\n            let mut path = String::from(l.split(' ').nth(2).unwrap_or(\"\"));\n            if path.starts_with(&prefix) {\n                path = path.replace(&prefix, \"\");\n            } else {\n                path = format!(\"{}{}\", cdup, path);\n            }\n            ret.push(path);\n        }\n        ret.sort();\n        Ok(ret)\n    }\n\n    fn show_cdup(opt: &Opt) -> Result<String, Error> {\n        let args = vec![String::from(\"rev-parse\"), String::from(\"--show-cdup\")];\n\n        let output = CmdGit::call(&opt, &args)?;\n\n        let mut list = str::from_utf8(&output.stdout)\n            .context(GitError::ConvFailed {\n                s: output.stdout.to_vec(),\n            })?\n            .lines();\n        Ok(String::from(list.next().unwrap_or(\"\")))\n    }\n\n    fn show_prefix(opt: &Opt) -> Result<String, Error> {\n        let args = vec![String::from(\"rev-parse\"), String::from(\"--show-prefix\")];\n\n        let output = CmdGit::call(&opt, &args)?;\n\n        let mut list = str::from_utf8(&output.stdout)\n            .context(GitError::ConvFailed {\n                s: output.stdout.to_vec(),\n            })?\n            .lines();\n        Ok(String::from(list.next().unwrap_or(\"\")))\n    }\n\n    fn get_cmd(opt: &Opt, args: &[String]) -> String {\n        let mut cmd = format!(\n            \"cd {}; {}\",\n            opt.dir.to_string_lossy(),\n            opt.bin_git.to_string_lossy()\n        );\n        for arg in args {\n            cmd = format!(\"{} {}\", cmd, arg);\n        }\n        cmd\n    }\n}\n\n// ---------------------------------------------------------------------------------------------------------------------\n// Test\n// ---------------------------------------------------------------------------------------------------------------------\n\n#[cfg(test)]\nmod tests {\n    use super::CmdGit;\n    use crate::bin::Opt;\n    use std::fs;\n    use std::io::{BufWriter, Write};\n    use structopt::StructOpt;\n\n    static TRACKED_FILES: [&'static str; 23] = [\n        \".cargo/config\",\n        \".gitattributes\",\n        \".github/FUNDING.yml\",\n        \".github/dependabot.yml\",\n        \".github/workflows/dependabot_merge.yml\",\n        \".github/workflows/periodic.yml\",\n        \".github/workflows/regression.yml\",\n        \".github/workflows/release.yml\",\n        \".gitignore\",\n        \".gitmodules\",\n        \"Cargo.lock\",\n        \"Cargo.toml\",\n        \"LICENSE\",\n        \"Makefile\",\n        \"README.md\",\n        \"benches/ptags_bench.rs\",\n        \"src/bin.rs\",\n        \"src/cmd_ctags.rs\",\n        \"src/cmd_git.rs\",\n        \"src/lib.rs\",\n        \"src/main.rs\",\n        \"test/lfs.txt\",\n        \"test/ptags_test\",\n    ];\n\n    #[test]\n    fn test_get_files() {\n        let args = vec![\"ptags\"];\n        let opt = Opt::from_iter(args.iter());\n        let files = CmdGit::get_files(&opt).unwrap();\n        assert_eq!(files, TRACKED_FILES,);\n    }\n\n    #[test]\n    fn test_get_files_exclude_lfs() {\n        let args = vec![\"ptags\", \"--exclude-lfs\"];\n        let opt = Opt::from_iter(args.iter());\n        let files = CmdGit::get_files(&opt).unwrap();\n\n        let mut expect_files = Vec::new();\n        expect_files.extend_from_slice(&TRACKED_FILES);\n        let idx = expect_files.binary_search(&\"test/lfs.txt\").unwrap();\n        expect_files.remove(idx);\n\n        assert_eq!(files, expect_files,);\n    }\n\n    #[test]\n    fn test_get_files_exclude_lfs_cd() {\n        let args = vec![\"ptags\", \"--exclude-lfs\", \"src\"];\n        let opt = Opt::from_iter(args.iter());\n        let files = CmdGit::get_files(&opt).unwrap();\n        assert_eq!(\n            files,\n            vec![\"bin.rs\", \"cmd_ctags.rs\", \"cmd_git.rs\", \"lib.rs\", \"main.rs\"]\n        );\n    }\n\n    #[test]\n    fn test_get_files_include_ignored() {\n        {\n            let mut f = BufWriter::new(fs::File::create(\"ignored.gz\").unwrap());\n            let _ = f.write(b\"\");\n        }\n        let args = vec![\"ptags\", \"--include-ignored\"];\n        let opt = Opt::from_iter(args.iter());\n        let files: Vec<String> = CmdGit::get_files(&opt)\n            .unwrap()\n            .into_iter()\n            .filter(|f| !f.starts_with(\"target/\"))\n            .collect();\n        let _ = fs::remove_file(\"ignored.gz\");\n\n        let mut expect_files = Vec::new();\n        expect_files.push(\"ignored.gz\");\n        expect_files.push(\"tags\");\n\n        assert_eq!(files, expect_files,);\n    }\n\n    #[test]\n    fn test_get_files_include_submodule() {\n        let args = vec![\"ptags\", \"--include-submodule\"];\n        let opt = Opt::from_iter(args.iter());\n        let files = CmdGit::get_files(&opt).unwrap();\n\n        let mut expect_files = Vec::new();\n        expect_files.extend_from_slice(&TRACKED_FILES);\n        let idx = expect_files.binary_search(&\"test/ptags_test\").unwrap();\n        expect_files.remove(idx);\n        expect_files.push(\"test/ptags_test/README.md\");\n\n        assert_eq!(files, expect_files,);\n    }\n\n    #[test]\n    fn test_get_files_include_untracked() {\n        {\n            let mut f = BufWriter::new(fs::File::create(\"tmp\").unwrap());\n            let _ = f.write(b\"\");\n        }\n        let args = vec![\"ptags\", \"--include-untracked\"];\n        let opt = Opt::from_iter(args.iter());\n        let files = CmdGit::get_files(&opt).unwrap();\n        let _ = fs::remove_file(\"tmp\");\n\n        let mut expect_files = Vec::new();\n        expect_files.extend_from_slice(&TRACKED_FILES);\n        expect_files.push(\"tmp\");\n\n        assert_eq!(files, expect_files,);\n    }\n\n    #[test]\n    fn test_command_fail() {\n        let args = vec![\"ptags\", \"--bin-git\", \"aaa\"];\n        let opt = Opt::from_iter(args.iter());\n        let files = CmdGit::ls_files(&opt);\n        assert_eq!(\n            &format!(\"{:?}\", files)[0..42],\n            \"Err(failed to call git command (cd .; aaa \"\n        );\n    }\n\n    #[test]\n    fn test_git_fail() {\n        let args = vec![\"ptags\", \"--opt-git=-aaa\"];\n        let opt = Opt::from_iter(args.iter());\n        let files = CmdGit::ls_files(&opt);\n        assert_eq!(\n            &format!(\"{:?}\", files)[0..83],\n            \"Err(failed to execute git command (cd .; git ls-files --cached --exclude-standard -\"\n        );\n    }\n}\n"
  },
  {
    "path": "src/lib.rs",
    "content": "pub mod bin;\npub mod cmd_ctags;\npub mod cmd_git;\n"
  },
  {
    "path": "src/main.rs",
    "content": "use ptagslib::bin::run;\n\n// ---------------------------------------------------------------------------------------------------------------------\n// Main\n// ---------------------------------------------------------------------------------------------------------------------\n\nfn main() {\n    match run() {\n        Err(x) => {\n            println!(\"{}\", x);\n            for x in x.chain() {\n                println!(\"{}\", x);\n            }\n        }\n        _ => (),\n    }\n}\n"
  },
  {
    "path": "test/lfs.txt",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:17e682f060b5f8e47ea04c5c4855908b0a5ad612022260fe50e11ecb0cc0ab76\nsize 4\n"
  }
]