text: /*
   * Copyright (c) Meta Platforms, Inc. and affiliates.
   *
   * This source code is licensed under the MIT license found in the
   * LICENSE file in the root directory of this source tree.
   */
  
  #include "watchman/Client.h"
  #include "watchman/Command.h"
  #include "watchman/CommandRegistry.h"
  #include "watchman/Errors.h"
  #include "watchman/LogConfig.h"
  #include "watchman/QueryableView.h"
  #include "watchman/root/Root.h"
  #include "watchman/root/watchlist.h"
  #include "watchman/watchman_cmd.h"
  
  using namespace watchman;
  
  void w_cmd_realpath_root(Command& command) {
    std::vector<json_ref> args = command.args().array();
  
    if (args.empty()) {
      throw CommandValidationError("wrong number of arguments");
    }
  
    const char* path = json_string_value(args[0]);
    if (!path) {
      throw CommandValidationError(
          "second argument must be a string expressing the path to the watch");
    }
  
    try {
      auto resolved = realPath(path);
      args[0] = w_string_to_json(resolved);
    } catch (const std::exception& exc) {
      throw CommandValidationError(
          "Could not resolve ",
          path,
          " to the canonical watch path: ",
          exc.what());
    }
  
    command.args() = json_array(std::move(args));
  }
  W_CAP_REG("clock-sync-timeout")
  
  /* Add the current clock value to the response */
  static void annotate_with_clock(
      const std::shared_ptr<Root>& root,
      json_ref& resp) {
    resp.set("clock", w_string_to_json(root->view()->getCurrentClockString()));
  }
  
  /* clock /root [options]
   * Returns the current clock value for a watched root
   * If the options contain a sync_timeout, we ensure that the repo
   * is synced up-to-date and the returned clock represents the
   * latest state.
   */
  static json_ref cmd_clock(Client* client, const json_ref& args) {
    int sync_timeout = 0;
  
    // TODO: merge this parse and sync logic with the logic in query evaluation
    if (json_array_size(args) == 3) {
      auto& opts = args.at(2);
      if (!opts.isObject()) {
        throw ErrorResponse(
            "the third argument to 'clock' must be an optional object");
      }
  
      auto sync = opts.get_default("sync_timeout");
      if (sync) {
        if (!sync.isInt()) {
          throw ErrorResponse(
              "the sync_timeout option passed to 'clock' must be an integer");
        }
        sync_timeout = sync.asInt();
      }
    } else if (json_array_size(args) != 2) {
      throw ErrorResponse("wrong number of arguments to 'clock'");
    }
  
    /* resolve the root */
    auto root = resolveRoot(client, args);
  
    if (sync_timeout) {
      root->syncToNow(std::chrono::milliseconds(sync_timeout));
    }
  
    auto resp = make_response();
    annotate_with_clock(root, resp);
  
    return resp;
  }
  W_CMD_REG(
      "clock",
      cmd_clock,
      CMD_DAEMON | CMD_ALLOW_ANY_USER,
      w_cmd_realpath_root);
  
  /* watch-del /root
   * Stops watching the specified root */
  static json_ref cmd_watch_delete(Client* client, const json_ref& args) {
    /* resolve the root */
    if (json_array_size(args) != 2) {
      throw ErrorResponse("wrong number of arguments to 'watch-del'");
    }
  
    auto root = resolveRoot(client, args);
  
    auto resp = make_response();
    resp.set(
        {{"watch-del", json_boolean(root->stopWatch())},
         {"root", w_string_to_json(root->root_path)}});
    return resp;
  }
  W_CMD_REG("watch-del", cmd_watch_delete, CMD_DAEMON, w_cmd_realpath_root);
  
  /* watch-del-all
   * Stops watching all roots */
  static json_ref cmd_watch_del_all(Client*, const json_ref&) {
    auto resp = make_response();
    auto roots = w_root_stop_watch_all();
    resp.set("roots", std::move(roots));
    return resp;
  }
  W_CMD_REG(
      "watch-del-all",
      cmd_watch_del_all,
      CMD_DAEMON | CMD_POISON_IMMUNE,
      NULL);
  
  /* watch-list
   * Returns a list of watched roots */
  static json_ref cmd_watch_list(Client*, const json_ref&) {
    auto resp = make_response();
    auto root_paths = w_root_watch_list_to_json();
    resp.set("roots", std::move(root_paths));
    return resp;
  }
  W_CMD_REG("watch-list", cmd_watch_list, CMD_DAEMON | CMD_ALLOW_ANY_USER, NULL);
  
  // For each directory component in candidate_dir to the root of the filesystem,
  // look for root_file.  If root_file is present, update relpath to reflect the
  // relative path to the original value of candidate_dir and return true.  If
  // not found, return false. candidate_dir is modified by this function if
  // return true.
  static bool find_file_in_dir_tree(
      const w_string& root_file,
      w_string_piece& candidate_dir,
      w_string_piece& relpath) {
    w_string_piece current_dir(candidate_dir);
    while (true) {
      auto projPath = w_string::pathCat({current_dir, root_file});
  
      if (w_path_exists(projPath.c_str())) {
        // Got a match
        relpath = w_string_piece(candidate_dir);
        if (candidate_dir.size() == current_dir.size()) {
          relpath = w_string_piece();
        } else {
          relpath.advance(current_dir.size() + 1);
          candidate_dir = current_dir;
        }
        return true;
      }
  
      auto parent = current_dir.dirName();
      if (parent == nullptr || parent == current_dir) {
        return false;
      }
      current_dir = parent;
    }
    return false;
  }
  
  bool find_project_root(
      const json_ref& root_files,
      w_string_piece& resolved,
      w_string_piece& relpath) {
    uint32_t i;
  
    for (i = 0; i < json_array_size(root_files); i++) {
      auto item = json_array_get(root_files, i);
      auto name = json_to_w_string(item);
  
      if (find_file_in_dir_tree(name, resolved, relpath)) {
        return true;
      }
    }
    return false;
  }
  
  // For watch-project, take a root path string and resolve the
  // containing project directory, then update the args to reflect
  // that path.
  // relpath will hold the path to the project dir, relative to the
  // watched dir.  If it is NULL it means that the project dir is
  // equivalent to the watched dir.
  static w_string resolve_projpath(
      std::vector<json_ref>& args,
      w_string& relpath) {
    const char* path;
    bool enforcing;
    if (args.size() < 2) {
      throw CommandValidationError("wrong number of arguments");
    }
  
    path = json_string_value(args[1]);
    if (!path) {
      throw CommandValidationError("second argument must be a string");
    }
  
    auto resolved = realPath(path);
  
    auto root_files = cfg_compute_root_files(&enforcing);
    if (!root_files) {
      throw CommandValidationError(
          "resolve_projpath: error computing root_files configuration value, "
          "consult your log file at ",
          logging::log_name,
          " for more details");
    }
  
    // See if we're requesting something in a pre-existing watch
  
    w_string_piece prefix;
    w_string_piece relpiece;
    if (findEnclosingRoot(resolved, prefix, relpiece)) {
      relpath = relpiece.asWString();
      resolved = prefix.asWString();
      args[1] = w_string_to_json(resolved);
      return resolved;
    }
    auto resolvedpiece = resolved.piece();
    if (find_project_root(root_files, resolvedpiece, relpiece)) {
      relpath = relpiece.asWString();
      resolved = resolvedpiece.asWString();
      args[1] = w_string_to_json(resolved);
      return resolved;
    }
  
    if (!enforcing) {
      // We'll use the path they originally requested
      return resolved;
    }
  
    // Convert root files to comma delimited string for error message
    auto root_files_list = cfg_pretty_print_root_files(root_files);
  
    throw CommandValidationError(
        "resolve_projpath:  None of the files listed in global config "
        "root_files are present in path `",
        path,
        "` or any of its "
        "parent directories.  root_files is defined by the "
        "`",
        cfg_get_global_config_file_path().view(),
        "` config file and includes ",
        root_files_list,
        ".  "
        "One or more of these files must be present in order to allow "
        "a watch. Try pulling and checking out a newer version of the project?");
  }
  
  /* watch /root */
  static json_ref cmd_watch(Client* client, const json_ref& args) {
    /* resolve the root */
    if (json_array_size(args) != 2) {
      throw ErrorResponse("wrong number of arguments to 'watch'");
    }
  
    auto root = resolveOrCreateRoot(client, args);
    root->view()->waitUntilReadyToQuery().get();
  
    auto resp = make_response();
  
    if (root->failure_reason) {
      resp.set("error", w_string_to_json(root->failure_reason));
    } else if (root->inner.cancelled) {
      resp.set(
          "error", typed_string_to_json("root was cancelled", W_STRING_UNICODE));
    } else {
      resp.set(
          {{"watch", w_string_to_json(root->root_path)},
           {"watcher", w_string_to_json(root->view()->getName())}});
    }
    add_root_warnings_to_response(resp, root);
    return resp;
  }
  W_CMD_REG(
      "watch",
      cmd_watch,
      CMD_DAEMON | CMD_ALLOW_ANY_USER,
      w_cmd_realpath_root);
  
  static json_ref cmd_watch_project(Client* client, const json_ref& args) {
    /* resolve the root */
    if (json_array_size(args) != 2) {
      throw ErrorResponse("wrong number of arguments to 'watch-project'");
    }
  
    w_string rel_path_from_watch;
    std::vector<json_ref> args_array = args.array();
    auto dir_to_watch = resolve_projpath(args_array, rel_path_from_watch);
    auto root = resolveOrCreateRoot(client, json_array(std::move(args_array)));
  
    root->view()->waitUntilReadyToQuery().get();
  
    auto resp = make_response();
  
    if (root->failure_reason) {
      resp.set("error", w_string_to_json(root->failure_reason));
    } else if (root->inner.cancelled) {
      resp.set(
          "error", typed_string_to_json("root was cancelled", W_STRING_UNICODE));
    } else {
      resp.set(
          {{"watch", w_string_to_json(root->root_path)},
           {"watcher", w_string_to_json(root->view()->getName())}});
    }
    add_root_warnings_to_response(resp, root);
    if (!rel_path_from_watch.empty()) {
      resp.set("relative_path", w_string_to_json(rel_path_from_watch));
    }
    return resp;
  }
  W_CMD_REG(
      "watch-project",
      cmd_watch_project,
      CMD_DAEMON | CMD_ALLOW_ANY_USER,
      w_cmd_realpath_root);
  
  /* vim:ts=2:sw=2:et:
   */
  