@ 260927.1
current
#include <hypergrep/git_index_search.hpp>
#include <hypergrep/is_binary.hpp>
#include <unordered_set>

git_index_search::git_index_search(std::string &pattern,
                                   const std::filesystem::path &path,
                                   argparse::ArgumentParser &program)
    : basepath(std::filesystem::relative(path)) {
  initialize_search(pattern, program, options, &database, &scratch,
                    &file_filter_database, &file_filter_scratch);
}

git_index_search::git_index_search(hs_database_t *database,
                                   hs_scratch_t *scratch,
                                   hs_database_t *file_filter_database,
                                   hs_scratch_t *file_filter_scratch,
                                   const search_options &options,
                                   const std::filesystem::path &path)
    : basepath(path), database(database), scratch(scratch),
      file_filter_database(file_filter_database),
      file_filter_scratch(file_filter_scratch), options(options) {
  non_owning_database = true;
}

git_index_search::~git_index_search() {
  if (!non_owning_database) {
    if (scratch) {
      hs_free_scratch(scratch);
    }
    if (database) {
      hs_free_database(database);
    }
    if (file_filter_scratch) {
      hs_free_scratch(file_filter_scratch);
    }
    if (file_filter_database) {
      hs_free_database(file_filter_database);
    }
  }

  for (auto &iter : garbage_collect_index_iterator) {
    if (iter)
      git_index_iterator_free(iter);
  }
  for (auto &idx : garbage_collect_index) {
    if (idx)
      git_index_free(idx);
  }
  for (auto &repo : garbage_collect_repo) {
    if (repo)
      git_repository_free(repo);
  }
}

void git_index_search::search_thread_function() {
  char buffer[FILE_CHUNK_SIZE];
  std::string lines{};

  hs_scratch_t *local_scratch = NULL;
  hs_error_t database_error = hs_alloc_scratch(database, &local_scratch);
  if (database_error != HS_SUCCESS) {
    throw std::runtime_error("Error allocating scratch space\n");
  }

  while (true) {
    if (num_files_enqueued > 0) {
      try_dequeue_and_process_path(local_scratch, buffer, lines);
    }
    if (!running && num_files_dequeued == num_files_enqueued) {
      break;
    }
  }

  hs_free_scratch(local_scratch);
}

void git_index_search::run(std::filesystem::path path) {
  if (!libgit2_initialized) {
    git_libgit2_init();
    libgit2_initialized = true;
  }

  std::vector<std::thread> consumer_threads(options.num_threads);
  if (options.perform_search) {
    for (std::size_t i = 0; i < options.num_threads; ++i) {
      consumer_threads[i] = std::thread(
          std::bind(&git_index_search::search_thread_function, this));
    }
  }

  visit_git_repo(path);

  running = false;

  // Done enqueuing files for search

  // Help with the search now:
  if (options.perform_search) {
    search_thread_function();

    for (std::size_t i = 0; i < options.num_threads; ++i) {
      consumer_threads[i].join();
    }
  }

  // Process submodules
  const auto current_path = std::filesystem::current_path();
  for (const auto &sm_path : submodule_paths) {
    git_index_search git_index_searcher(
        database, scratch, file_filter_database, file_filter_scratch, options,
        basepath /
            std::filesystem::relative(std::filesystem::canonical(sm_path)));
    if (chdir(sm_path.c_str()) == 0) {
      git_index_searcher.run(".");
      if (chdir(current_path.c_str()) != 0) {
        throw std::runtime_error("Failed to restore path");
      }
    }
  }
}

bool git_index_search::process_file(const char *filename,
                                    hs_scratch_t *local_scratch, char *buffer,
                                    std::string &lines) {
  int fd = open(filename, O_RDONLY, 0);
  if (fd == -1) {
    std::cerr << filename << ": " << std::strerror(errno) << " (os error " << errno << ")\n";
    return false;
  }
  bool result{false};

  const auto process_fn =
      (options.is_stdout || options.print_only_matching_parts ||
       options.show_column_numbers || options.show_byte_offset)
          ? process_matches
          : process_matches_nocolor_nostdout;
  auto result_path = basepath / filename;

  // Process the file in chunks
  std::size_t total_bytes_read = 0;
  bool max_file_size_provided = options.max_file_size.has_value();
  std::size_t max_file_size =
      max_file_size_provided ? options.max_file_size.value() : 0;
  std::size_t bytes_read = 0;
  std::atomic<std::size_t> max_line_number{0};
  std::size_t current_line_number{1};
  std::size_t num_matching_lines{0};
  std::size_t num_matches{0};

  // Read the file in chunks and perform search
  bool first{true};
  while (true) {

    auto ret = read(fd, buffer, FILE_CHUNK_SIZE);

    if (ret > 0) {
      bytes_read = ret;
    } else {
      break;
    }

    total_bytes_read += bytes_read;

    if (max_file_size_provided && total_bytes_read > max_file_size) {
      // File size limit reached
      close(fd);
      lines.clear();
      return false;
    }

    if (first) {
      first = false;
      if (starts_with_magic_bytes(buffer, bytes_read)) {
        result = false;
        break;
      }

      if (has_null_bytes(buffer, bytes_read)) {
        // NULL bytes found
        // Ignore file
        // Could be a .exe, .gz, .bin etc.
        result = false;
        break;
      }
    }

    // Detect end of file
    // If end of file, search entire chunk
    // If not end of file, search for last newline
    const auto last_chunk = (bytes_read < FILE_CHUNK_SIZE);
    std::size_t search_size = bytes_read;

    char *last_newline = buffer + bytes_read;
    if (!last_chunk) {
      // Find the position of the last newline in the buffer
      // In order to catch matches between chunks, need to amend the buffer
      // and make sure it stops at a new line boundary
      char *last_newline = (char *)memrchr(buffer, '\n', bytes_read);
      if (last_newline) {
        search_size = last_newline - buffer;
      } else {
        // Not a single newline in the entire chunk
        // This could be some binary file or some minified JS file
        // Skip it
        result = false;
        break;
      }
    }

    std::vector<std::pair<unsigned long long, unsigned long long>> matches{};
    std::atomic<size_t> number_of_matches = 0;
    file_context ctx{number_of_matches, matches, options.print_only_filenames};

    if (hs_scan(database, buffer, search_size, 0, local_scratch, on_match,
                (void *)(&ctx)) != HS_SUCCESS) {
      if (options.print_only_filenames && ctx.number_of_matches > 0) {
        result = true;
      } else {
        result = false;
      }
      break;
    } else {
      if (ctx.number_of_matches > 0) {
        result = true;
      }
    }

    if (ctx.number_of_matches > 0) {
      num_matching_lines += process_fn(
          result_path.c_str(), buffer, search_size, ctx.matches,
          current_line_number, lines, options.print_filenames,
          options.is_stdout, options.show_line_numbers,
          options.show_column_numbers, options.show_byte_offset,
          options.print_only_matching_parts, options.max_column_limit,
          total_bytes_read - bytes_read, options.ltrim_each_output_line);
      num_matches += ctx.number_of_matches;
    }

    if (!last_chunk) {

      if (static_cast<std::size_t>(last_newline - buffer) == bytes_read - 1) {
        // Chunk ends exactly at the newline
        // Do nothing
      } else {
        // Backtrack "remainder" number of characters
        if ((bytes_read - search_size) < FILE_CHUNK_SIZE) {
          lseek(fd, -1 * (bytes_read - search_size), SEEK_CUR);
        } else {
          // Don't lseek back the entire chunk
          // because that's an infinite loop

          // Not a single newline in the entire chunk
          // This could be some binary file or some minified JS file
          // Skip it
          result = false;
          break;
        }
      }
    }
  }

  close(fd);

  if ((result || options.count_include_zeros) && options.count_matching_lines &&
      !options.print_only_filenames) {
    if (options.print_filenames) {
      if (options.is_stdout) {
        fmt::print(
            "{}:{}\n",
            fmt::format(fg(fmt::color::steel_blue), "{}", result_path.c_str()),
            num_matching_lines);
      } else {
        fmt::print("{}:{}\n", result_path.c_str(), num_matching_lines);
      }
    } else {
      fmt::print("{}\n", num_matching_lines);
    }
  } else if ((result || options.count_include_zeros) && options.count_matches &&
             !options.print_only_filenames) {
    if (options.print_filenames) {
      if (options.is_stdout) {
        fmt::print(
            "{}:{}\n",
            fmt::format(fg(fmt::color::steel_blue), "{}", result_path.c_str()),
            num_matches);
      } else {
        fmt::print("{}:{}\n", result_path.c_str(), num_matches);
      }
    } else {
      fmt::print("{}\n", num_matches);
    }
  } else {
    if (result && options.print_only_filenames) {
      if (options.is_stdout) {
        fmt::print(fg(fmt::color::steel_blue), "{}\n", result_path.c_str());
      } else {
        fmt::print("{}\n", result_path.c_str());
      }
    } else if (result && !options.count_matching_lines &&
               !options.count_matches && !options.print_only_filenames &&
               !lines.empty()) {
      if (options.is_stdout) {
        if (options.print_filenames) {
          lines = fmt::format(fg(fmt::color::steel_blue), "\n{}\n",
                              result_path.c_str()) +
                  lines;
        } else {
          lines += "\n";
        }
        fmt::print("{}", lines);
      } else {
        fmt::print("{}", lines);
      }
    }
  }

  lines.clear();
  return result;
}

bool git_index_search::search_submodules(const char *dir,
                                         git_repository *this_repo) {

  if (options.exclude_submodules) {
    return true;
  }

  struct context {
    git_index_search *ptr;
    const char *dir;
  };

  context ctx{this, dir};

  if (git_submodule_foreach(
          this_repo,
          [](git_submodule *, const char *name, void *payload) -> int {
            context *ctx = (context *)(payload);
            auto self = ctx->ptr;
            auto dir = ctx->dir;

            auto path = std::filesystem::path(dir) / name;
            if (std::filesystem::exists(path / ".git")) {
              self->submodule_paths.push_back(path);
            } else {
              // submodule not recursively cloned
              // ignore
            }

            return 0;
          },
          (void *)&ctx) != 0) {
    return false;
  } else {
    return true;
  }
}

bool git_index_search::visit_git_index(const std::filesystem::path &,
                                       git_index *index) {
  git_index_iterator *iter{nullptr};
  if (git_index_iterator_new(&iter, index) == 0) {

    if (iter)
      garbage_collect_index_iterator.push_back(iter);

    const git_index_entry *entry = nullptr;
    while (git_index_iterator_next(&entry, iter) != GIT_ITEROVER) {
      if (entry &&
          (!options.filter_files ||
           (options.filter_files &&
            filter_file(entry->path, file_filter_database, file_filter_scratch,
                        options.negate_filter)))) {

        // Skip directories and symlinks
        if ((entry->mode & S_IFMT) == S_IFDIR ||
            (entry->mode & S_IFMT) == S_IFLNK) {
          continue;
        }

        if (!options.search_hidden_files) {
          if (entry->path[0] == '.') {
            continue;
          }
          std::string_view path(entry->path);
          auto it = path.find_last_of("/");
          if (it != std::string_view::npos) {
            // Found a '/'
            if (path[it + 1] == '.') {
              continue;
            }
          }
        }

        if (options.perform_search) {
          queue.enqueue(ptok, entry->path);
          ++num_files_enqueued;
        } else {

          // If --max-filesize is used, perform a stat
          // and check if the file size is under the limit
          static const bool max_file_size_provided =
              options.max_file_size.has_value();
          static const std::size_t max_file_size =
              max_file_size_provided ? options.max_file_size.value() : 0;

          if (max_file_size_provided) {

            // NOTE: Possible optimization here is to use entry->file_size
            // instead of making a stat call
            //
            // Making the stat call is the more accurate approach since
            // these files could have unstaged changes on disk.

            const auto file_size = std::filesystem::file_size(entry->path);
            if (file_size > max_file_size) {
              // skip this file
              continue;
            }
          }

          if (options.is_stdout) {
            fmt::print(fg(fmt::color::steel_blue), "./{}\n", entry->path);
          } else {
            fmt::print("./{}\n", entry->path);
          }
        }
      }
    }
    return true;
  } else {
    return false;
  }
}

bool git_index_search::visit_git_repo(const std::filesystem::path &dir,
                                      git_repository *repo) {
  bool result{true};

  // Open the repository
  if (!repo) {
    if (git_repository_open(&repo, dir.c_str()) != 0) {
      result = false;
    }
  }

  if (result)
    garbage_collect_repo.push_back(repo);

  // Load the git index for this repository
  git_index *index = nullptr;
  if (result && git_repository_index(&index, repo) != 0) {
    result = false;
  }

  if (result)
    garbage_collect_index.push_back(index);

  // Visit each entry in the index
  if (result && !visit_git_index(dir, index)) {
    result = false;
  }

  // Search the submodules inside this submodule
  if (result && !search_submodules(dir.c_str(), repo)) {
    result = false;
  }
  return result;
}

bool git_index_search::try_dequeue_and_process_path(hs_scratch_t *local_scratch,
                                                    char *buffer,
                                                    std::string &lines) {
  constexpr std::size_t BULK_DEQUEUE_SIZE = 32;
  const char *entries[BULK_DEQUEUE_SIZE];
  auto count =
      queue.try_dequeue_bulk_from_producer(ptok, entries, BULK_DEQUEUE_SIZE);
  if (count > 0) {
    for (std::size_t j = 0; j < count; ++j) {
      process_file(entries[j], local_scratch, buffer, lines);
    }
    num_files_dequeued += count;
    return true;
  }
  return false;
}