diff --git a/.github/workflows/doc-build.yml b/.github/workflows/doc-build.yml new file mode 100644 index 0000000..628bb24 --- /dev/null +++ b/.github/workflows/doc-build.yml @@ -0,0 +1,10 @@ +name: Build API Docs + +on: + workflow_dispatch: + +jobs: + call-doc-build-workflow: + uses: clojure/build.ci/.github/workflows/doc-build.yml@master + with: + project: clojure/tools.gitlibs diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..e2718bd --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,19 @@ +name: Release on demand + +on: + workflow_dispatch: + inputs: + releaseVersion: + description: "Version to release" + required: true + snapshotVersion: + description: "Snapshot version after release" + required: true + +jobs: + call-release: + uses: clojure/build.ci/.github/workflows/release.yml@master + with: + releaseVersion: ${{ github.event.inputs.releaseVersion }} + snapshotVersion: ${{ github.event.inputs.snapshotVersion }} + secrets: inherit \ No newline at end of file diff --git a/.github/workflows/snapshot.yml b/.github/workflows/snapshot.yml new file mode 100644 index 0000000..2472957 --- /dev/null +++ b/.github/workflows/snapshot.yml @@ -0,0 +1,8 @@ +name: Snapshot on demand + +on: [workflow_dispatch] + +jobs: + call-snapshot: + uses: clojure/build.ci/.github/workflows/snapshot.yml@master + secrets: inherit diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..1fa127c --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,7 @@ +name: Test + +on: [push] + +jobs: + call-test: + uses: clojure/build.ci/.github/workflows/test.yml@master diff --git a/.gitignore b/.gitignore index ce07fda..62df01d 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,7 @@ target/ .nrepl* .cpcache .lein* +.calva/ +.clj-kondo/ +.lsp/ +.vscode/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 6ec6377..83a1097 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,49 @@ Changelog =========== +* 2.6.212 on Dec 30, 2025 + * Bump parent pom to latest +* 2.6.206 on Dec 5, 2024 + * Bump clojure dep to 1.11.4 + * Improve error message when clone fails to include url +* 2.5.197 on May 31, 2023 + * TDEPS-248 - Make `tags` return known tags when offline +* 2.5.190 on Feb 12, 2023 + * Don't use future to background the process reading (leaves non-daemon thread) +* 2.5.186 on Feb 11, 2023 + * Don't block on big git output +* 2.4.181 on Jun 19, 2022 + * Sort tags in version order +* 2.4.176 on Jun 16, 2022 + * Add `commit-sha` api function to return commit sha for rev +* 2.4.172 on Nov 8, 2021 + * TDEPS-212 - Support local file repos +* 2.3.167 on Apr 22, 2021 + * Remove use of `--prune-tags` with `git fetch` as that was only added in git 2.17.0 +* 2.3.161 on Apr 9, 2021 + * Add new `object-type` api - takes rev, returns object type +* 2.2.156 on Apr 7, 2021 + * `tags` api should fetch to ensure all tags are returned +* 2.2.152 on Apr 3, 2021 + * Fix issue with fetching new commits on branches +* 2.1.144 on Mar 12, 2021 + * Fix rev-parse check on unfetched sha to report failure +* 2.1.139 on Mar 11, 2021 + * Add tags method +* 2.1.129 on Mar 10, 2021 + * Change git options during checkout to work with more git versions + * Check exit code on checkout and throw if failed + * Overhaul config and debug settings + * Change min Clojure dep to 1.9 +* 2.0.119 on Mar 10, 2021 + * Fix issues with checkouts of multiple commits per repo + * resolve now only fetches if it can't resolve a ref + * Config read from most to least preferred: Java system property / env var / default: + * gitlibs dir: `clojure.gitlibs.dir` / `GITLIBS` / `nil` + * git command: `clojure.gitlibs.command` / `GITLIBS_COMMAND` / `"git"` + * Fix reflection error +* 2.0.109 on Mar 3, 2021 + * TDEPS-91 Replace jgit implementation by shelling out to git * 1.0.100 on Aug 20, 2020 * Fetch all tags on procure * 1.0.96 on Aug 7, 2020 diff --git a/README.md b/README.md index de4704a..5125bde 100644 --- a/README.md +++ b/README.md @@ -9,56 +9,48 @@ To access git dependencies (for example, via tools.deps), one must download git and working trees as indicated by git shas. This library provides this functionality and also keeps a cache of git dirs and working trees that can be reused. -## Usage +## API The following API is provided in `clojure.tools.gitlibs`: * `(resolve git-url rev) ;; returns full sha of rev in git-url` * `(procure git-url lib rev) ;; returns working tree directory for git-url identified as lib at rev` * `(descendant git-url revs) ;; returns rev which is a descedant of all revs, or nil if none` +* `(tags git-url) ;; returns a collection of tags in this git-url (fetches to refresh)` +* `(cache-dir) ;; returns path to root of gitlibs cache dir` ### Git urls The following git url types are supported: -* `https` - for public anonymous clone and fetch of public repos -* `ssh` - for authenticated clone and fetch of private repos -* `http` and `git` protocols are plain-text and not supported or recommended - -### SSH authentication for private repositories - -ssh authentication works by connecting to the local ssh agent (ssh-agent on *nix or Pageant via PuTTY on Windows). -The ssh-agent must have a registered identity for the key being used to access the Git repository. -To check whether you have registered identities, use: - -`ssh-add -l` - -which should return one or more registered identities, typically the one at `~/.ssh/id_rsa`. - -For more information on creating keys and using the ssh-agent to manage your ssh identities, GitHub provides excellent info: - -* https://help.github.com/articles/generating-a-new-ssh-key-and-adding-it-to-the-ssh-agent/ -* https://help.github.com/articles/working-with-ssh-key-passphrases/ - -*Note: user/password authentication is not supported for any protocol.* +* `https` - for public anonymous clone and fetch of public or private repos with credentials via git credential sources +* `ssh` - for authenticated clone and fetch of private repos (uses ssh) +* `http` and `git` protocols are plain-text and NOT supported or recommended ### Revs The API functions all take revs, which can be any git rev that resolves to a commit, such as: * Full sha (40 chars) -* Prefix sha (sufficiently unique in the repo) +* Prefix sha (sufficiently unique in the repo, often 7 chars) * Tag name * Branch name Procured working trees are always cached on the basis of the rev's full sha, so using `procure` repeatedly on a rev that does not resolve to a fixed sha may result in new checkouts in the cache. -### Cache directory +### Configuration + +Downloaded git dirs and working trees are stored in the gitlibs cache dir, ~/.gitlibs by default. This directory is just a cache and can be safely removed if needed. -Downloaded git dirs and working trees are stored in ~/.gitlibs - this directory is just a cache and can be safely removed if needed. +tools.gitlibs can be configured by either environment variable or Java system property. If both are provided, the Java system property takes precedence. -The cache location can also be set with the environment variable GITLIBS. +| Env var | Java system property | default | description +| ------- | -------------------- | ------- | ----------- +| GITLIBS | clojure.gitlibs.dir | ~/.gitlibs | Local directory cache for git repos and working trees | +| GITLIBS_COMMAND | clojure.gitlibs.command | git | git command to run when shelling out (supply full path if needed) | +| GITLIBS_DEBUG | clojure.gitlibs.debug | false | If true, print git commands and output to stderr | +| GITLIBS_TERMINAL | clojure.gitlibs.terminal | false | If true, interactively prompt if needed | ## Example Usage @@ -83,23 +75,21 @@ The cache location can also be set with the environment variable GITLIBS. This project follows the version scheme MAJOR.MINOR.COMMITS where MAJOR and MINOR provide some relative indication of the size of the change, but do not follow semantic versioning. In general, all changes endeavor to be non-breaking (by moving to new names rather than by breaking existing names). COMMITS is an ever-increasing counter of commits since the beginning of this repository. -Latest release: 1.0.100 +Latest release: 2.6.212 * [All released versions](http://search.maven.org/#search%7Cgav%7C1%7Cg%3A%22org.clojure%22%20AND%20a%3A%22tools.gitlibs%22) -* Coordinates: `org.clojure/tools.gitlibs {:mvn/version "1.0.100"}` +* Coordinates: `org.clojure/tools.gitlibs {:mvn/version "2.6.212"}` # Developer Information * [GitHub project](https://github.com/clojure/tools.gitlibs) -* [API Docs](https://clojure.github.io/tools.gitlibs) -* [How to contribute](https://dev.clojure.org/display/community/Contributing) -* [Bug Tracker](https://dev.clojure.org/jira/browse/TDEPS) -* [Continuous Integration](https://build.clojure.org/job/gitlibs/) -* [Compatibility Test Matrix](https://build.clojure.org/job/tools.gitlibs-matrix/) +* [Bug Tracker](https://dev.clojure.org/jira/browse/TDEPS) - if you don't have an acct there, please ask at [Ask Clojure](https://ask.clojure.org) +* [How to contribute](https://clojure.org/dev/dev) +* [Continuous Integration](https://github.com/clojure/tools.gitlibs/actions/workflows/test.yml) # Copyright and License -Copyright © 2018-2020 Rich Hickey, Alex Miller, and contributors +Copyright © Rich Hickey, Alex Miller, and contributors All rights reserved. The use and distribution terms for this software are covered by the @@ -109,4 +99,4 @@ in any fashion, you are agreeing to be bound by the terms of this license. You must not remove this notice, or any other, from this software. -[Eclipse Public License 1.0]: http://opensource.org/licenses/eclipse-1.0.php +[Eclipse Public License 1.0]: https://opensource.org/license/epl-1-0/ diff --git a/VERSION_TEMPLATE b/VERSION_TEMPLATE index a201147..0cfa6a1 100755 --- a/VERSION_TEMPLATE +++ b/VERSION_TEMPLATE @@ -1 +1 @@ -1.0.GENERATED_VERSION +2.6.GENERATED_VERSION diff --git a/deps.edn b/deps.edn index 9341351..1fafc86 100644 --- a/deps.edn +++ b/deps.edn @@ -1,2 +1,2 @@ {:paths ["src/main/clojure"] - :deps {org.clojure/clojure {:mvn/version "1.8.0"}}} + :deps {org.clojure/clojure {:mvn/version "1.11.4"}}} diff --git a/pom.xml b/pom.xml index 9483784..61e9342 100644 --- a/pom.xml +++ b/pom.xml @@ -1,13 +1,14 @@ 4.0.0 tools.gitlibs - 1.0.101-SNAPSHOT + 2.6.213-SNAPSHOT tools.gitlibs + API for retrieving, caching, and programatically accessing git libraries from Clojure. org.clojure pom.contrib - 0.3.0 + 1.4.0 @@ -19,7 +20,7 @@ true - 1.8.0 + 1.11.4 diff --git a/src/main/clojure/clojure/tools/gitlibs.clj b/src/main/clojure/clojure/tools/gitlibs.clj index f8d9b03..3662a9b 100644 --- a/src/main/clojure/clojure/tools/gitlibs.clj +++ b/src/main/clojure/clojure/tools/gitlibs.clj @@ -14,69 +14,104 @@ (:refer-clojure :exclude [resolve]) (:require [clojure.java.io :as jio] + [clojure.tools.gitlibs.config :as config] [clojure.tools.gitlibs.impl :as impl])) +(set! *warn-on-reflection* true) + (defn cache-dir "Return the root gitlibs cache directory. By default ~/.gitlibs or override by setting the environment variable GITLIBS." [] - (impl/cache-dir)) + (:gitlibs/dir @config/CONFIG)) + +;; Possible new API, internal for now +(defn- resolve-all + "Takes a git url and a coll of revs, and returns the full commit shas. + Each rev may be a partial sha, full sha, or tag name. Returns nil for + unresolveable revs." + [url revs] + (let [git-dir (impl/ensure-git-dir url)] + (reduce + (fn [rs r] + (if-let [res (impl/git-rev-parse git-dir r)] + (conj rs res) + (do ;; could not resolve - fetch and try again + (impl/git-fetch (jio/file git-dir)) + (conj rs (impl/git-rev-parse git-dir r))))) + [] revs))) (defn resolve - "Takes a git url and a rev, and returns the full commit sha. rev may be a - partial sha, full sha, or tag name. + "Takes a git url and a rev, and returns the full commit sha or nil if can't + resolve. rev may be a partial sha, full sha, or tag name." + [url rev] + (first (resolve-all url [rev]))) - Optional opts map may include: - :interactive (default false) - set true to allow stdin prompts (example: unknown host) - :print-commands (default false) - set true to write git executions to stderr" - ([url rev] - (resolve url rev nil)) - ([url rev opts] - (impl/git-rev-parse (impl/ensure-git-dir url opts) rev opts))) +(defn object-type + "Takes a git url and rev, and returns the object type, one of :tag :tree + :commit or :blob, or nil if not known or ambiguous." + [url rev] + (let [git-dir (impl/ensure-git-dir url)] + (if-let [type (impl/git-type git-dir rev)] + type + (do + (impl/git-fetch (jio/file git-dir)) + (impl/git-type git-dir rev))))) (defn procure "Procure a working tree at rev for the git url representing the library lib, returns the directory path. lib is a qualified symbol where the qualifier is a - controlled or conveyed identity, or nil if rev is unknown. - - Optional opts map may include: - :interactive (default false) - set true to allow stdin prompts (example: unknown host) - :print-commands (default false) - set true to write git commands to stderr" - ([url lib rev] - (procure url lib rev nil)) - ([url lib rev opts] - (let [lib-dir (impl/lib-dir lib) - sha (or (impl/match-exact lib-dir rev) (impl/match-prefix lib-dir rev) (resolve url rev))] - (when sha - (let [sha-dir (jio/file lib-dir sha)] - (when-not (.exists sha-dir) - (impl/printerrln "Checking out:" url "at" rev) - (impl/git-checkout url sha-dir sha opts)) - (.getCanonicalPath sha-dir)))))) + controlled or conveyed identity, or nil if rev is unknown." + [url lib rev] + (let [lib-dir (impl/lib-dir lib) + git-dir-path (impl/ensure-git-dir url) + sha (or (impl/match-exact lib-dir rev) (impl/match-prefix lib-dir rev) (resolve url rev))] + (when sha + (let [sha-dir (jio/file lib-dir sha)] + (when-not (.exists sha-dir) + (impl/printerrln "Checking out:" url "at" rev) + (impl/git-checkout git-dir-path lib-dir sha)) + (.getCanonicalPath sha-dir))))) (defn descendant "Returns rev in git url which is a descendant of all other revs, - or nil if no such relationship can be established. - - Optional opts map may include: - :interactive (default false) - set true to allow stdin prompts (example: unknown host) - :print-commands (default false) - set true to write git commands to stderr" - ([url rev] - (descendant url rev nil)) - ([url revs opts] - (when (seq revs) - (let [git-dir (impl/ensure-git-dir url opts) - shas (map #(impl/git-rev-parse git-dir % opts) revs)] - (if (not-empty (filter nil? shas)) - nil ;; can't resolve all shas in this repo - (first (sort (partial impl/commit-comparator git-dir opts) shas))))))) + or nil if no such relationship can be established." + [url revs] + (when (seq revs) + (let [shas (resolve-all url revs)] + (if (seq (filter nil? shas)) + nil ;; can't resolve all shas in this repo + (let [git-dir (impl/ensure-git-dir url)] + (->> shas (sort (partial impl/commit-comparator git-dir)) first)))))) + +(defn tags + "Fetches, then returns coll of tags in git url" + [url] + (impl/tags (impl/ensure-git-dir url))) + +(defn commit-sha + "Returns unpeeled full commit sha, given a rev (which may be tag, branch, etc)" + [url rev] + (impl/git-rev-parse (impl/ensure-git-dir url) rev)) (comment - (resolve "git@github.com:clojure/tools.gitlibs.git" "11fc774" {:print-commands true :interactive true}) - (descendant "https://github.com/clojure/tools.gitlibs.git" ["5e2797a487c" "11fc774" "d82adc29" "815e312310"] {:print-commands true}) + (commit-sha "https://github.com/clojure/tools.build.git" "v0.8.2") + + (System/setProperty "clojure.gitlibs.debug" "true") + (resolve "git@github.com:clojure/tools.gitlibs.git" "11fc774") + (descendant "https://github.com/clojure/tools.gitlibs.git" ["5e2797a487c" "11fc774" "d82adc29" "815e312310"]) (println @(future (procure "https://github.com/clojure/tools.gitlibs.git" 'org.clojure/tools.gitlibs "11fc77496f013871c8af3514bbba03de0af28061")) @(future (procure "https://github.com/clojure/tools.gitlibs.git" 'org.clojure/tools.gitlibs "11fc77496f013871c8af3514bbba03de0af28061"))) - ) \ No newline at end of file + (println + @(future (procure "https://github.com/cognitect-labs/test-runner.git" 'cognitect-labs/test-runner "b6b3193fcc42659d7e46ecd1884a228993441182")) + @(future (procure "https://github.com/cognitect-labs/test-runner.git" 'cognitect-labs/test-runner "cb96e80f6f3d3b307c59cbeb49bb0dcb3a2a780b")) + @(future (procure "https://github.com/cognitect-labs/test-runner.git" 'cognitect-labs/test-runner "9e1098965f2089c8cf492d23c0b7520f8690440a"))) + + (tags "https://github.com/clojure/tools.gitlibs.git") + + ;; big output + (tags "https://github.com/confluentinc/kafka-streams-examples.git") + ) diff --git a/src/main/clojure/clojure/tools/gitlibs/config.clj b/src/main/clojure/clojure/tools/gitlibs/config.clj new file mode 100644 index 0000000..ab9d8ac --- /dev/null +++ b/src/main/clojure/clojure/tools/gitlibs/config.clj @@ -0,0 +1,48 @@ +; Copyright (c) Rich Hickey. All rights reserved. +; The use and distribution terms for this software are covered by the +; Eclipse Public License 1.0 (http://opensource.org/licenses/eclipse-1.0.php) +; which can be found in the file epl-v10.html at the root of this distribution. +; By using this software in any fashion, you are agreeing to be bound by +; the terms of this license. +; You must not remove this notice, or any other, from this software. + +(ns clojure.tools.gitlibs.config + "Implementation, use at your own risk" + (:require + [clojure.java.io :as jio] + [clojure.string :as str])) + +(set! *warn-on-reflection* true) + +(defn- read-config-value + "Read a config value from each of these in order, taking the first value found: + * Java system property + * env variable + * default value" + [property env default] + (or + (System/getProperty property) + (System/getenv env) + default)) + +(defn- init-config + [] + {:gitlibs/dir + (.getCanonicalPath + (let [lib-dir (or (System/getProperty "clojure.gitlibs.dir") (System/getenv "GITLIBS"))] + (if (str/blank? lib-dir) + (jio/file (System/getProperty "user.home") ".gitlibs") + (jio/file lib-dir)))) + + :gitlibs/command + (or (System/getProperty "clojure.gitlibs.command") (System/getenv "GITLIBS_COMMAND") "git") + + :gitlibs/debug + (Boolean/parseBoolean (or (System/getProperty "clojure.gitlibs.debug") (System/getenv "GITLIBS_DEBUG") "false")) + + :gitlibs/terminal + (Boolean/parseBoolean (or (System/getProperty "clojure.gitlibs.terminal") (System/getenv "GITLIBS_TERMINAL") "false"))}) + +(def CONFIG + "Config map - deref to access" + (delay (init-config))) \ No newline at end of file diff --git a/src/main/clojure/clojure/tools/gitlibs/impl.clj b/src/main/clojure/clojure/tools/gitlibs/impl.clj index b98e9a7..62e6ef9 100644 --- a/src/main/clojure/clojure/tools/gitlibs/impl.clj +++ b/src/main/clojure/clojure/tools/gitlibs/impl.clj @@ -10,9 +10,13 @@ "Implementation, use at your own risk" (:require [clojure.java.io :as jio] - [clojure.string :as str]) + [clojure.string :as str] + [clojure.tools.gitlibs.config :as config]) (:import - [java.io File FilenameFilter IOException])) + [java.lang ProcessBuilder$Redirect] + [java.io File FilenameFilter InputStream IOException StringWriter])) + +(set! *warn-on-reflection* true) ;; io util @@ -20,112 +24,158 @@ (binding [*out* *err*] (apply println msgs))) -(defn- runproc - [{:keys [interactive print-commands]} & args] - (when print-commands - (apply printerrln args)) - (let [proc-builder (ProcessBuilder. ^java.util.List args) - _ (when-not interactive (.put (.environment proc-builder) "GIT_TERMINAL_PROMPT" "0")) - proc (.start proc-builder) - exit (.waitFor proc) - out (slurp (.getInputStream proc)) - err (slurp (.getErrorStream proc))] - {:exit exit :out out :err err})) +(defn- capture + "Reads from input-stream until EOF and returns a String (or nil if 0 length)." + [^InputStream input-stream] + (let [writer (StringWriter.)] + (jio/copy input-stream writer) + (let [s (str/trim (.toString writer))] + (when-not (zero? (.length s)) + s)))) + +(defmacro background + [& body] + `(let [result# (promise)] + (doto (Thread. (fn [] (deliver result# (do ~@body)))) + (.setDaemon true) + (.start)) + result#)) + +(defn- run-git + [& args] + (let [{:gitlibs/keys [command debug terminal]} @config/CONFIG + command-args (cons command args)] + (when debug + (apply printerrln command-args)) + (let [proc-builder (ProcessBuilder. ^java.util.List command-args) + _ (when debug (.redirectError proc-builder ProcessBuilder$Redirect/INHERIT)) + _ (when-not terminal (.put (.environment proc-builder) "GIT_TERMINAL_PROMPT" "0")) + proc (.start proc-builder) + out (background (capture (.getInputStream proc))) + err (background (capture (.getErrorStream proc))) ;; if debug is true, stderr will be redirected instead + exit (.waitFor proc)] + {:args command-args, :exit exit, :out @out, :err @err}))) ;; dirs -(def ^:private CACHE - (delay - (.getCanonicalPath - (let [env (System/getenv "GITLIBS")] - (if (str/blank? env) - (jio/file (System/getProperty "user.home") ".gitlibs") - (jio/file env)))))) - -(defn cache-dir - "Absolute path to the root of the cache" - [] - @CACHE) - (defn lib-dir ^File [lib] - (jio/file (cache-dir) "libs" (namespace lib) (name lib))) + (jio/file (:gitlibs/dir @config/CONFIG) "libs" (namespace lib) (name lib))) + +(def ^:private git-url-regex + #"([a-z0-9+.-]+):\/\/(?:(?:(?:[^@]+?)@)?([^/]+?)(?::[0-9]*)?)?(/[^:]+)") + +(def ^:private git-scp-regex + #"(?:(?:[^@]+?)@)?(.+?):([^:]+)") (defn- clean-url - "Chop leading protocol, trailing .git, replace :'s with /" + "Convert url into a safe relative path (this is not a reversible transformation) + based on scheme, host, and path (drop user and port). + + Examples: + ssh://git@gitlab.com:3333/org/repo.git => ssh/gitlab.com/org/repo + git@github.com:dotted.org/dotted.repo.git => ssh/github.com/dotted.org/dotted.repo + file://../foo => file/REL/_DOTDOT_/foo + file:///Users/user/foo.git => file/Users/user/foo + ../foo => file/REL/_DOTDOT_/foo + ~user/foo.git => file/REL/_TILDE_user/foo + + * https://git-scm.com/docs/git-clone#_git_urls + * https://git-scm.com/book/en/v2/Git-on-the-Server-The-Protocols + " [url] - (-> url - (str/split #"://") - last - (str/replace #"\.git$" "") - (str/replace #":" "/"))) + (let [[scheme host path] (cond + (str/starts-with? url "file://") ["file" nil (-> url (subs 7) (str/replace #"^([^/])" "REL/$1"))] + (str/includes? url "://") (let [[_ s h p] (re-matches git-url-regex url)] [s h p]) + (str/includes? url ":") (let [[_ h p] (re-matches git-scp-regex url)] ["ssh" h p]) + :local-repo ["file" nil (str/replace url #"^([^/])" "REL/$1")]) + clean-path (-> path + (str/replace #"\.git/?$" "") ;; remove trailing .git or .git/ + (str/replace #"~" "_TILDE_")) ;; replace ~ with _TILDE_ + dir-parts (->> (concat [scheme host] (str/split clean-path #"/")) ;; split on / + (remove str/blank?) ;; remove any missing path segments + (map #(-> % ({"." "_DOT_", ".." "_DOTDOT_"} %))))] ;; replace . or .. segments + (str/join "/" dir-parts))) (defn git-dir ^File [url] - (jio/file (cache-dir) "_repos" (clean-url url))) + (jio/file (:gitlibs/dir @config/CONFIG) "_repos" (clean-url url))) -;; git clone --bare --quiet URL PATH -;; git --git-dir <> fetch -;; git --git-dir <> --work-tree checkout +(defn git-try-fetch + "Try to fetch and return the error code (0=success)" + [^File git-dir] + (let [git-path (.getCanonicalPath git-dir) + ;; NOTE: --prune-tags would be desirable here but was added in git 2.17.0 + {:keys [exit]} (run-git "--git-dir" git-path + "fetch" "--quiet" "--all" "--tags" "--prune")] + exit)) (defn git-fetch - [^File git-dir opts] + [^File git-dir] (let [git-path (.getCanonicalPath git-dir) - {:keys [exit err] :as ret} (runproc opts "git" "--git-dir" git-path "fetch" "--tags")] + ;; NOTE: --prune-tags would be desirable here but was added in git 2.17.0 + {:keys [exit err] :as ret} (run-git "--git-dir" git-path + "fetch" "--quiet" "--all" "--tags" "--prune")] (when-not (zero? exit) (throw (ex-info (format "Unable to fetch %s%n%s" git-path err) ret))))) ;; TODO: restrict clone to an optional refspec? (defn git-clone-bare - [url ^File git-dir opts] + [url ^File git-dir] (printerrln "Cloning:" url) (let [git-path (.getCanonicalPath git-dir) - {:keys [exit err] :as ret} (runproc opts "git" "clone" "--bare" url git-path)] + {:keys [exit err] :as ret} (run-git "clone" "--quiet" "--mirror" url git-path)] (when-not (zero? exit) - (throw (ex-info (format "Unable to clone %s%n%s" git-path err) ret))) + (throw (ex-info (format "Unable to clone %s to %s:%n%s" url git-path err) ret))) git-dir)) (defn ensure-git-dir "Ensure the bare git dir for the specified url, return the path to the git dir." - [url opts] - (let [git-dir-file (git-dir url)] - (if (.exists git-dir-file) - (git-fetch git-dir-file opts) - (git-clone-bare url git-dir-file opts)) + [url] + (let [git-dir-file (git-dir url) + config-file (jio/file git-dir-file "config")] + (when-not (.exists config-file) + (git-clone-bare url git-dir-file)) (.getCanonicalPath git-dir-file))) (defn git-checkout - [url ^File rev-dir ^String rev opts] - (when-not (.exists rev-dir) - (.mkdirs rev-dir)) - (runproc opts - "git" - "--git-dir" (ensure-git-dir url opts) - "--work-tree" (.getCanonicalPath rev-dir) - "checkout" rev)) + [git-dir-path ^File lib-dir ^String rev] + (let [rev-file (jio/file lib-dir rev)] + (when-not (.exists rev-file) + (let [{:keys [exit err] :as ret} + (run-git "--git-dir" git-dir-path + "worktree" "add" "--force" "--detach" + (.getCanonicalPath rev-file) rev)] + (when-not (zero? exit) + (throw (ex-info (format "Unable to checkout %s%n%s" rev err) ret))))))) (defn git-rev-parse - [git-dir rev opts] - (let [{:keys [exit out]} (runproc opts "git" "--git-dir" git-dir "rev-parse" rev)] + [git-dir rev] + (let [{:keys [exit out]} (run-git "--git-dir" git-dir "rev-parse" (str rev "^{commit}"))] (when (zero? exit) (str/trimr out)))) -;; git merge-base --is-ancestor +(defn git-type + [git-dir rev] + (let [{:keys [exit out]} (run-git "--git-dir" git-dir "cat-file" "-t" rev)] + (when (zero? exit) + (keyword (str/trimr out))))) + +;; git merge-base --is-ancestor (defn- ancestor? - [git-dir x y opts] - (let [args ["git" "--git-dir" git-dir "merge-base" "--is-ancestor" x y] - {:keys [exit err] :as ret} (apply runproc opts args)] + [git-dir x y] + (let [{:keys [exit err] :as ret} (run-git "--git-dir" git-dir "merge-base" "--is-ancestor" x y)] (condp = exit 0 true 1 false - (throw (ex-info (format "Unable to compare commits %s%n%s" (.getCanonicalPath git-dir) err) ret))))) + (throw (ex-info (format "Unable to compare commits %s%n%s" git-dir err) ret))))) (defn commit-comparator - [git-dir opts x y] + [git-dir x y] (cond (= x y) 0 - (ancestor? git-dir x y opts) 1 - (ancestor? git-dir y x opts) -1 + (ancestor? git-dir x y) 1 + (ancestor? git-dir y x) -1 :else (throw (ex-info "" {})))) (defn match-exact @@ -148,3 +198,12 @@ 0 nil 1 (.getName ^File (aget matches 0)) (throw (IOException. (str "Prefix not unique: " prefix)))))))) + +(defn tags + "Fetch, then return all tags in the git dir." + [git-dir] + (git-try-fetch (jio/file git-dir)) + (let [{:keys [exit out err] :as ret} (run-git "--git-dir" git-dir "tag" "--sort=v:refname")] + (when-not (zero? exit) + (throw (ex-info (format "Unable to get tags %s%n%s" git-dir err) ret))) + (remove str/blank? (str/split-lines out)))) diff --git a/src/test/clojure/clojure/tools/gitlibs/test_impl.clj b/src/test/clojure/clojure/tools/gitlibs/test_impl.clj new file mode 100644 index 0000000..ea732cd --- /dev/null +++ b/src/test/clojure/clojure/tools/gitlibs/test_impl.clj @@ -0,0 +1,36 @@ +(ns clojure.tools.gitlibs.test-impl + (:require + [clojure.test :refer :all] + [clojure.tools.gitlibs.impl :as impl])) + +(deftest test-clean-url + (are [url expected-path] + (= expected-path (#'impl/clean-url url)) + + ;; url formats - don't use user or port + "ssh://git@gitlab.com:3333/org/repo.git" "ssh/gitlab.com/org/repo" + "ssh://git@gitlab.org.net/org/repo.git" "ssh/gitlab.org.net/org/repo" + "ssh://user@host.xz/~user/repo.git/" "ssh/host.xz/_TILDE_user/repo" + "https://github.com/org/repo.git" "https/github.com/org/repo" + "git://host.xz/path/to/repo.git/" "git/host.xz/path/to/repo" + + ;; scp style url (most common github ssh url format) + "git@github.com:org/repo.git" "ssh/github.com/org/repo" + "git@github.com:dotted.org/dotted.repo.git" "ssh/github.com/dotted.org/dotted.repo" + "host.xz:~user/path/to/repo.git/" "ssh/host.xz/_TILDE_user/path/to/repo" + + ;; file scheme + "file:///Users/me/code/repo.git" "file/Users/me/code/repo" + "file://../foo.git" "file/REL/_DOTDOT_/foo" + "file://~/path/repo.git" "file/REL/_TILDE_/path/repo" + + ;; file repos - handle relative vs absolute, handle . .. ~ + "/Users/me/code/repo.git" "file/Users/me/code/repo" + "../foo.git" "file/REL/_DOTDOT_/foo" + "./foo.git" "file/REL/_DOT_/foo" + "~user/foo.git" "file/REL/_TILDE_user/foo" + + ;; git - unknown transport with url rewrite in gitconfig (unusual, but do something useful) + "work:repo.git" "ssh/work/repo")) + + diff --git a/src/test/clojure/clojure/tools/test_gitlibs.clj b/src/test/clojure/clojure/tools/test_gitlibs.clj index c14e9bc..7c6b39a 100644 --- a/src/test/clojure/clojure/tools/test_gitlibs.clj +++ b/src/test/clojure/clojure/tools/test_gitlibs.clj @@ -10,8 +10,7 @@ (:require [clojure.java.io :as jio] [clojure.test :refer :all] - [clojure.tools.gitlibs :as gl] - [clojure.tools.gitlibs.impl :as glim])) + [clojure.tools.gitlibs :as gl])) (def repo-url "https://github.com/clojure/spec.alpha.git") @@ -20,9 +19,11 @@ "739c1af56dae621aedf1bb282025a0d676eff713"))) (deftest test-procure - (let [wt (gl/procure repo-url 'org.clojure/spec.alpha "739c1af")] - (is (= wt (.getAbsolutePath (jio/file (glim/cache-dir) "libs" "org.clojure" "spec.alpha" "739c1af56dae621aedf1bb282025a0d676eff713")))) - (is (.exists (jio/file (glim/cache-dir) "_repos" "github.com" "clojure" "spec.alpha"))))) + (let [wt1 (gl/procure repo-url 'org.clojure/spec.alpha "739c1af") + wt2 (gl/procure repo-url 'org.clojure/spec.alpha "6a56327")] + (is (.exists (jio/file (gl/cache-dir) "_repos" "https" "github.com" "clojure" "spec.alpha"))) + (is (= wt1 (.getAbsolutePath (jio/file (gl/cache-dir) "libs" "org.clojure" "spec.alpha" "739c1af56dae621aedf1bb282025a0d676eff713")))) + (is (= wt2 (.getAbsolutePath (jio/file (gl/cache-dir) "libs" "org.clojure" "spec.alpha" "6a56327446c909db0d11ecf93a3c3d659b739be9")))))) (deftest test-descendant-fixed (is (= (gl/descendant repo-url ["607aef0" "739c1af"])