|
1 | 1 | (ns noumenon.git |
2 | | - (:require [clojure.edn :as edn] |
3 | | - [clojure.java.io :as io] |
| 2 | + (:require [clojure.java.io :as io] |
4 | 3 | [clojure.java.shell :as shell] |
5 | 4 | [clojure.string :as str] |
6 | 5 | [datomic.client.api :as d] |
|
91 | 90 | regex-based hostname extractor returns nil for those, so its |
92 | 91 | `when-let` would otherwise short-circuit and leave the URL |
93 | 92 | unchecked. Perforce depot paths follow a different code path |
94 | | - (`p4-clone!`) and never reach this validator. |
| 93 | + (`noumenon.p4/clone!`) and never reach this validator. |
95 | 94 |
|
96 | 95 | Both throws carry `:status 400` so HTTP handlers render bad URLs as |
97 | 96 | 400, not as the route handler's default 500 fallback." |
|
467 | 466 | {:commits-imported (count to-import) |
468 | 467 | :commits-skipped skipped |
469 | 468 | :elapsed-ms elapsed})))) |
470 | | - |
471 | | -;; --- Perforce via git-p4 --- |
472 | | - |
473 | | -(defn p4-depot-path? |
474 | | - "True if s looks like a Perforce depot path (starts with //)." |
475 | | - [s] |
476 | | - (boolean (and (string? s) (str/starts-with? s "//")))) |
477 | | - |
478 | | -(defn p4-available? |
479 | | - "True if git-p4 is available (git p4 subcommand works)." |
480 | | - [] |
481 | | - (zero? (:exit (shell/sh "git" "p4" "--help")))) |
482 | | - |
483 | | -(defn p4-clone? |
484 | | - "True if repo-path is a git-p4 clone (has p4 remote refs)." |
485 | | - [repo-path] |
486 | | - (let [p4-refs (io/file repo-path ".git" "refs" "remotes" "p4")] |
487 | | - (and (.isDirectory p4-refs) |
488 | | - (pos? (count (.list p4-refs)))))) |
489 | | - |
490 | | -(defn p4-depot->clone-name |
491 | | - "Derive a local clone directory name from a depot path. |
492 | | - //depot/ProjectA/main/... -> ProjectA-main |
493 | | - //stream/main/... -> main |
494 | | - //depot/... -> depot" |
495 | | - [depot-path] |
496 | | - (let [segments (-> depot-path |
497 | | - (str/replace #"^//" "") |
498 | | - (str/replace #"/\.\.\.$" "") |
499 | | - (str/replace #"/$" "") |
500 | | - (str/split #"/")) |
501 | | - ;; Use sub-path segments if available, else fall back to depot name |
502 | | - name-parts (if (> (count segments) 1) (rest segments) segments)] |
503 | | - (-> (str/join "-" name-parts) |
504 | | - (str/replace #"[^a-zA-Z0-9\-_.]" "")))) |
505 | | - |
506 | | -(defn p4-clone-path |
507 | | - "Local clone path for a depot path: data/repos/<derived-name>." |
508 | | - [depot-path] |
509 | | - (str "data/repos/" (p4-depot->clone-name depot-path))) |
510 | | - |
511 | | -(def ^:private default-p4-excludes |
512 | | - "Default binary exclusion patterns, loaded from p4-excludes.edn." |
513 | | - (delay |
514 | | - (some-> (io/resource "p4-excludes.edn") slurp edn/read-string))) |
515 | | - |
516 | | -(defn p4-exclude-patterns |
517 | | - "Compute the final exclusion pattern list given options. |
518 | | - Options: |
519 | | - :no-default-excludes? — skip default patterns (default false) |
520 | | - :extra-excludes — additional patterns to exclude (vector of strings) |
521 | | - :includes — patterns to remove from excludes (vector of strings)" |
522 | | - [{:keys [no-default-excludes? extra-excludes includes]}] |
523 | | - (let [defaults (when-not no-default-excludes? |
524 | | - (->> (vals @default-p4-excludes) (apply concat))) |
525 | | - all (concat defaults extra-excludes) |
526 | | - include-set (set includes)] |
527 | | - (->> all (remove include-set) distinct vec))) |
528 | | - |
529 | | -(defn- p4-exclude-args |
530 | | - "Build -/ arguments for git p4 clone from a list of exclusion patterns." |
531 | | - [patterns] |
532 | | - (mapcat (fn [p] ["-/" p]) patterns)) |
533 | | - |
534 | | -(defn validate-p4-depot-path! |
535 | | - "Validate a Perforce depot path. Must start with //, contain only safe chars, |
536 | | - and have at least one depot name segment. Throws on invalid input." |
537 | | - [depot-path] |
538 | | - (when-not (and (string? depot-path) |
539 | | - (re-matches #"//[a-zA-Z0-9_\-./]+" depot-path) |
540 | | - (not (str/includes? depot-path ".."))) |
541 | | - (throw (ex-info "Invalid Perforce depot path" |
542 | | - {:depot-path depot-path |
543 | | - :message "Depot path must start with // and contain only alphanumeric, dash, underscore, dot, or slash characters"}))) |
544 | | - (when-not (p4-available?) |
545 | | - (throw (ex-info "git-p4 is not available. Install git-p4 to use Perforce support." |
546 | | - {:depot-path depot-path})))) |
547 | | - |
548 | | -(defn p4-clone! |
549 | | - "Clone a Perforce depot path via git-p4 into target-dir. |
550 | | - Options: |
551 | | - :excludes — override exclusion patterns (vector of strings) |
552 | | - :no-default-excludes? — skip default binary excludes |
553 | | - :extra-excludes — additional patterns to exclude |
554 | | - :includes — patterns to remove from default excludes |
555 | | - :use-client-spec? — use P4 workspace view for filtering |
556 | | - :max-changes — limit history depth (number of changelists)" |
557 | | - [depot-path target-dir opts] |
558 | | - (validate-p4-depot-path! depot-path) |
559 | | - (let [excludes (or (:excludes opts) |
560 | | - (p4-exclude-patterns opts)) |
561 | | - args (cond-> ["git" "p4" "clone"] |
562 | | - (:use-client-spec? opts) (conj "--use-client-spec") |
563 | | - (not (:use-client-spec? opts)) (into (p4-exclude-args excludes)) |
564 | | - (:max-changes opts) (conj (str "--max-changes=" (:max-changes opts))) |
565 | | - true (conj (str depot-path "@all")) |
566 | | - true (conj "--destination" (str target-dir))) |
567 | | - _ (log! (str "git-p4: cloning " depot-path " into " target-dir |
568 | | - " (" (count excludes) " exclusion patterns)")) |
569 | | - {:keys [exit err]} (apply shell/sh args)] |
570 | | - (when-not (zero? exit) |
571 | | - (throw (ex-info (str "git p4 clone failed: " (str/trim (or err ""))) |
572 | | - {:exit exit :depot-path depot-path :target target-dir |
573 | | - :stderr (when err (subs err 0 (min (count err) 500)))}))) |
574 | | - (log! "git-p4: clone complete") |
575 | | - target-dir)) |
576 | | - |
577 | | -(defn p4-sync! |
578 | | - "Sync a git-p4 clone with latest Perforce changelists. |
579 | | - Runs `git p4 sync` then `git p4 rebase`." |
580 | | - [repo-path] |
581 | | - (let [sync-args ["-C" (str repo-path) "p4" "sync"] |
582 | | - {:keys [exit err]} (apply shell/sh "git" sync-args)] |
583 | | - (when-not (zero? exit) |
584 | | - (throw (ex-info (str "git p4 sync failed: " (str/trim (or err ""))) |
585 | | - {:exit exit :repo-path (str repo-path)})))) |
586 | | - (let [rebase-args ["-C" (str repo-path) "p4" "rebase"] |
587 | | - {:keys [exit err]} (apply shell/sh "git" rebase-args)] |
588 | | - (when-not (zero? exit) |
589 | | - (throw (ex-info (str "git p4 rebase failed: " (str/trim (or err ""))) |
590 | | - {:exit exit :repo-path (str repo-path)})))) |
591 | | - (log! (str "git-p4: synced " repo-path)) |
592 | | - true) |
0 commit comments