Ctrl K
ComunidadGuías…

lib/git-session-attribution.js

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
// Connect working-tree changes to the Clay sessions that were active while they changed.

var crypto = require("crypto");
var fs = require("fs");
var path = require("path");
var config = require("./config");
var gitCli = require("./git-cli");
var utils = require("./utils");

var MAX_BASELINE_BYTES = 2 * 1024 * 1024;
var MAX_DIGEST_BYTES = 8 * 1024 * 1024;
var MAX_SESSION_RECORDS = 80;

function attachGitSessionAttribution(options) {
  var cwd = options.cwd;
  var getOsUserInfoForSession = options.getOsUserInfoForSession || function () { return null; };
  var storageDir = options.storageDir || path.join(config.CONFIG_DIR, "git-attribution");
  var storagePath = path.join(storageDir, utils.encodeCwd(cwd) + ".json");
  var state = loadState();

  function loadState() {
    try {
      var parsed = JSON.parse(fs.readFileSync(storagePath, "utf8"));
      if (parsed && parsed.version === 1 && parsed.sessions) return parsed;
    } catch (e) {}
    return { version: 1, sessions: {} };
  }

  function saveState() {
    try {
      fs.mkdirSync(storageDir, { recursive: true });
      var keys = Object.keys(state.sessions).sort(function (left, right) {
        return (state.sessions[right].lastChangedAt || 0) - (state.sessions[left].lastChangedAt || 0);
      });
      for (var i = MAX_SESSION_RECORDS; i < keys.length; i++) delete state.sessions[keys[i]];
      var temporaryPath = storagePath + ".tmp." + process.pid;
      fs.writeFileSync(temporaryPath, JSON.stringify(state, null, 2) + "\n");
      if (process.platform !== "win32") {
        try { fs.chmodSync(temporaryPath, 0o600); } catch (chmodError) {}
      }
      fs.renameSync(temporaryPath, storagePath);
    } catch (e) {
      console.error("[git-attribution] Unable to save session change data:", e.message);
    }
  }

  function sessionKey(session) {
    return session.cliSessionId || ("local:" + session.localId);
  }

  function digestPath(root, file) {
    var absolutePath = path.resolve(root, file.path);
    var rootPrefix = path.resolve(root) + path.sep;
    if (absolutePath.indexOf(rootPrefix) !== 0) return "invalid:" + file.code;
    try {
      var stat = fs.lstatSync(absolutePath);
      var hash = crypto.createHash("sha256");
      hash.update(file.code || "");
      if (stat.isSymbolicLink()) hash.update(fs.readlinkSync(absolutePath));
      else if (stat.isFile() && stat.size <= MAX_DIGEST_BYTES) hash.update(fs.readFileSync(absolutePath));
      else if (stat.isFile()) hash.update("large:" + stat.size + ":" + stat.mtimeMs);
      else hash.update("non-file");
      return hash.digest("hex");
    } catch (e) {
      return "missing:" + (file.code || "");
    }
  }

  function readBaseline(root, file) {
    var absolutePath = path.resolve(root, file.path);
    var rootPrefix = path.resolve(root) + path.sep;
    if (absolutePath.indexOf(rootPrefix) !== 0) return null;
    try {
      var stat = fs.lstatSync(absolutePath);
      var buffer;
      if (stat.isSymbolicLink()) buffer = Buffer.from(fs.readlinkSync(absolutePath), "utf8");
      else if (stat.isFile() && stat.size <= MAX_BASELINE_BYTES) buffer = fs.readFileSync(absolutePath);
      else return null;
      return { content: buffer.toString("base64"), binary: buffer.indexOf(0) !== -1 };
    } catch (e) {
      return { missing: true };
    }
  }

  function captureSnapshot(includeBaseline, osUserInfo) {
    var status = gitCli.getStatus(cwd, osUserInfo);
    if (!status.isRepository) return null;
    var files = {};
    for (var i = 0; i < status.files.length; i++) {
      var file = status.files[i];
      files[file.path] = {
        fingerprint: digestPath(status.root, file),
        baseline: includeBaseline ? readBaseline(status.root, file) : undefined,
      };
    }
    return { root: status.root, head: status.oid || null, files: files, capturedAt: Date.now() };
  }

  function beginTurn(session) {
    if (!session || session._gitAttributionTurn) return;
    try {
      var osUserInfo = getOsUserInfoForSession(session);
      var snapshot = captureSnapshot(false, osUserInfo);
      if (!snapshot) return;
      var key = sessionKey(session);
      var oldLocalKey = "local:" + session.localId;
      if (key !== oldLocalKey && state.sessions[oldLocalKey] && !state.sessions[key]) {
        state.sessions[key] = state.sessions[oldLocalKey];
        delete state.sessions[oldLocalKey];
      }
      if (!state.sessions[key]) {
        var baseline = captureSnapshot(true, osUserInfo);
        state.sessions[key] = {
          key: key,
          title: session.title || "Untitled session",
          vendor: session.vendor || null,
          startedAt: Date.now(),
          baseline: baseline,
          changedPaths: {},
        };
        saveState();
      }
      session._gitAttributionTurn = snapshot;
      session._gitAttributionOsUserInfo = osUserInfo;
    } catch (e) {
      console.error("[git-attribution] Unable to capture turn start:", e.message);
    }
  }

  function finishTurn(session) {
    if (!session || !session._gitAttributionTurn) return;
    var before = session._gitAttributionTurn;
    session._gitAttributionTurn = null;
    try {
      var after = captureSnapshot(false, session._gitAttributionOsUserInfo);
      session._gitAttributionOsUserInfo = null;
      if (!after) return;
      var key = sessionKey(session);
      var record = state.sessions[key] || state.sessions["local:" + session.localId];
      if (!record) return;
      record.title = session.title || record.title;
      record.vendor = session.vendor || record.vendor;
      var paths = {};
      Object.keys(before.files).forEach(function (filePath) { paths[filePath] = true; });
      Object.keys(after.files).forEach(function (filePath) { paths[filePath] = true; });
      var changedAt = Date.now();
      Object.keys(paths).forEach(function (filePath) {
        var oldFingerprint = before.files[filePath] ? before.files[filePath].fingerprint : null;
        var newFingerprint = after.files[filePath] ? after.files[filePath].fingerprint : null;
        if (oldFingerprint !== newFingerprint) record.changedPaths[filePath] = changedAt;
      });
      record.lastChangedAt = changedAt;
      saveState();
    } catch (e) {
      console.error("[git-attribution] Unable to capture turn result:", e.message);
    }
  }

  function decorateStatus(status, sessions) {
    if (!status || !status.isRepository) return status;
    var liveByKey = {};
    sessions.forEach(function (session) {
      liveByKey[sessionKey(session)] = session;
      liveByKey["local:" + session.localId] = session;
    });
    for (var i = 0; i < status.files.length; i++) {
      var file = status.files[i];
      var matches = [];
      Object.keys(state.sessions).forEach(function (key) {
        var record = state.sessions[key];
        if (!record.changedPaths || !record.changedPaths[file.path]) return;
        var live = liveByKey[key];
        if (!live) return;
        matches.push({
          key: key,
          sessionId: live ? live.localId : null,
          title: live ? (live.title || record.title) : record.title,
          vendor: record.vendor || null,
          changedAt: record.changedPaths[file.path],
          preExisting: !!(record.baseline && record.baseline.files && record.baseline.files[file.path]),
        });
      });
      matches.sort(function (left, right) { return right.changedAt - left.changedAt; });
      file.sessions = matches.slice(0, 4);
    }
    return status;
  }

  function getSessionBaselineDiff(key, filePath, osUserInfo) {
    var record = state.sessions[key];
    if (!record || !record.baseline) throw new Error("Session baseline is unavailable");
    var current = gitCli.readWorkingTreeFile(record.baseline.root || cwd, filePath);
    var saved = record.baseline.files && record.baseline.files[filePath];
    var oldFile;
    if (saved && saved.baseline) {
      oldFile = saved.baseline.missing
        ? { content: "", binary: false }
        : { content: Buffer.from(saved.baseline.content || "", "base64").toString("utf8"), binary: !!saved.baseline.binary };
    } else {
      oldFile = gitCli.getFileAtCommit(record.baseline.root || cwd, record.baseline.head, filePath, osUserInfo);
    }
    return {
      path: filePath,
      oldContent: oldFile.binary ? "" : oldFile.content,
      newContent: current.binary ? "" : current.content,
      binary: oldFile.binary || current.binary,
      sessionTitle: record.title,
    };
  }

  return {
    beginTurn: beginTurn,
    decorateStatus: decorateStatus,
    finishTurn: finishTurn,
    getSessionBaselineDiff: getSessionBaselineDiff,
  };
}

module.exports = { attachGitSessionAttribution: attachGitSessionAttribution };