├── .travis.yml ├── .gitignore ├── test └── clj_jgit │ └── test │ ├── core.clj │ ├── helpers.clj │ ├── internal.clj │ ├── porcelain.clj │ └── querying.clj ├── project.clj ├── src └── clj_jgit │ ├── util.clj │ ├── internal.clj │ ├── querying.clj │ └── porcelain.clj └── README.md /.travis.yml: -------------------------------------------------------------------------------- 1 | language: clojure 2 | lein: lein2 3 | jdk: 4 | - openjdk7 5 | - oraclejdk7 6 | - openjdk6 7 | script: "lein2 with-profile dev test" 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | pom.xml 3 | *jar 4 | /lib/ 5 | /classes/ 6 | .lein-deps-sum 7 | sandbox 8 | sandbox* 9 | /.lein-failures 10 | .settings/ccw.repl.cmdhistory.prefs 11 | .classpath 12 | .project 13 | .lein-repl-history 14 | /target 15 | .nrepl-port 16 | .idea 17 | *.iml 18 | pom.xml* 19 | -------------------------------------------------------------------------------- /test/clj_jgit/test/core.clj: -------------------------------------------------------------------------------- 1 | (ns clj-jgit.test.core 2 | (:use [clj-jgit.util]) 3 | (:use [clojure.test])) 4 | 5 | (deftest test-name-from-uri 6 | (let [uris ["ssh://example.com/~/www/project.git" 7 | "~/your/repo/path/project.git" 8 | "http://git.example.com/project.git" 9 | "https://git.example.com/project.git"]] 10 | (is (every? #(= %1 "project") (map #(name-from-uri %) uris)) 11 | "Repository name must be 'project'"))) 12 | -------------------------------------------------------------------------------- /test/clj_jgit/test/helpers.clj: -------------------------------------------------------------------------------- 1 | (ns clj-jgit.test.helpers 2 | (:require [fs.core :as fs]) 3 | (:use clj-jgit.porcelain)) 4 | 5 | ; Using clj-jgit repo for read-only tests 6 | (def read-only-repo-path (System/getProperty "user.dir")) 7 | 8 | (defmacro read-only-repo [& body] 9 | `(with-repo ~read-only-repo-path 10 | ~@body)) 11 | 12 | (defmacro with-tmp-repo 13 | "execute operations within a created and immediately deleted repo, 14 | producing only the results of expressions in body." 15 | [repo-path & body] 16 | `(do 17 | (fs/delete-dir ~repo-path) 18 | (fs/mkdir ~repo-path) 19 | (git-init ~repo-path) 20 | (let [outcome# (with-repo ~repo-path ~@body)] 21 | (fs/delete-dir ~repo-path) 22 | outcome#))) -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject clj-jgit "0.8.7b" 2 | :description "Clojure wrapper for JGit" 3 | :dependencies [[org.eclipse.jgit/org.eclipse.jgit.java7 "3.7.0.201502260915-r" :exclusions [com.jcraft/jsch]] 4 | [fs "1.3.2"] 5 | [com.jcraft/jsch "0.1.52"]] 6 | :profiles {:dev {:dependencies [[org.clojure/clojure "1.5.1"] 7 | [midje "1.5.1"] 8 | [com.stuartsierra/lazytest "1.2.3"] 9 | [lein-clojars "0.9.0"]]} } 10 | :plugins [[lein-midje "3.0.1"] 11 | [lein-marginalia "0.7.1"]] 12 | :repositories {"stuartsierra-releases" "http://stuartsierra.com/maven2" 13 | "jgit-repository" "https://repo.eclipse.org/content/groups/releases/"}) 14 | -------------------------------------------------------------------------------- /src/clj_jgit/util.clj: -------------------------------------------------------------------------------- 1 | (ns clj-jgit.util) 2 | 3 | (defn name-from-uri 4 | "Given a URI to a Git resource, derive the name (for use in cloning to a directory)" 5 | [uri] 6 | (second (re-find #"/([^/]*)\.git$" uri))) 7 | 8 | (defmacro when-present 9 | "Special `when` macro for checking if an attribute isn't available or is an empty string" 10 | [obj & body] 11 | `(when (not (or (nil? ~obj) (empty? ~obj))) 12 | ~@body)) 13 | 14 | (defmethod print-method org.eclipse.jgit.internal.storage.file.RefDirectory$LooseRef 15 | [o w] 16 | (print-simple 17 | (str "#<" (.replaceFirst (str (.getClass o)) "class " "") ", " 18 | "Name: " (.getName o) ", " 19 | "ObjectId: " (.getName (.getObjectId o)) ">") w)) 20 | 21 | (defn normalize-path 22 | "Removes a leading slash from a path" 23 | [path] 24 | (if (= path "/") 25 | "/" 26 | (if (= (first path) \/) 27 | (apply str (rest path)) 28 | path))) 29 | 30 | (defn person-ident [^org.eclipse.jgit.lib.PersonIdent person] 31 | (when person 32 | {:name (.getName person) 33 | :email (.getEmailAddress person) 34 | :timezone (.getTimeZone person)})) 35 | -------------------------------------------------------------------------------- /test/clj_jgit/test/internal.clj: -------------------------------------------------------------------------------- 1 | (ns clj-jgit.test.internal 2 | (:use 3 | [clj-jgit.test.helpers] 4 | [clj-jgit.porcelain] 5 | [clj-jgit.querying] 6 | [clj-jgit.internal] 7 | [clojure.test]) 8 | (:import 9 | [org.eclipse.jgit.lib ObjectId] 10 | [org.eclipse.jgit.revwalk RevWalk RevCommit] 11 | [org.eclipse.jgit.treewalk TreeWalk])) 12 | 13 | (deftest internal-tests 14 | (testing "resolve-object" 15 | (read-only-repo 16 | (are 17 | [object-name] (instance? ObjectId (resolve-object object-name repo)) 18 | "master" ; commit-ish 19 | "38dd57264cf5c05fb77211c8347d1f16e4474623" ; initial commit 20 | "cefa1a770d57f7f89a59d1a376ef5ffc480649ae" ; tree 21 | "1656b6ddae437f8cbdaabaa27e399cb431eec94e" ; blob 22 | ))) 23 | 24 | (testing "bound-commit" 25 | (read-only-repo 26 | (are 27 | [commit-ish] (instance? RevCommit 28 | (bound-commit repo 29 | (new-rev-walk repo) 30 | (resolve-object commit-ish repo))) 31 | "38dd57264cf5c05fb77211c8347d1f16e4474623" ; initial commit 32 | "master" ; branch name 33 | "master^" ; commit before master's head 34 | ))) 35 | 36 | (testing "new-tree-walk" 37 | (read-only-repo 38 | (is (instance? TreeWalk (new-tree-walk repo (find-rev-commit repo (new-rev-walk repo) "master"))))))) -------------------------------------------------------------------------------- /test/clj_jgit/test/porcelain.clj: -------------------------------------------------------------------------------- 1 | (ns clj-jgit.test.porcelain 2 | (:use [clj-jgit.test.helpers] 3 | [clj-jgit.porcelain] 4 | [clojure.test]) 5 | (:import 6 | [java.io File] 7 | [org.eclipse.jgit.api Git] 8 | [org.eclipse.jgit.revwalk RevWalk])) 9 | 10 | (defn- get-temp-dir 11 | "Returns a temporary directory" 12 | [] 13 | (let [temp (File/createTempFile "test" "repo")] 14 | (if (.exists temp) 15 | (do 16 | (.delete temp) 17 | (.mkdir temp) 18 | (.deleteOnExit temp))) 19 | temp)) 20 | 21 | (deftest test-git-init 22 | (let [repo-dir (get-temp-dir)] 23 | (is #(nil? %) (git-init repo-dir)))) 24 | 25 | (deftest porcelain-tests 26 | (testing "with-repo macro" 27 | (read-only-repo 28 | (is (instance? Git repo)) 29 | (is (instance? RevWalk rev-walk))))) 30 | 31 | (deftest test-current-branch-functions 32 | (is (= [true 33 | "master" 34 | 40 35 | false] 36 | (with-tmp-repo "target/tmp" 37 | (let [tmp-file "target/tmp/tmp.txt"] 38 | (spit tmp-file "1") 39 | (git-add repo tmp-file) 40 | (git-commit repo "first commit") 41 | (let [sha (->> repo git-log first str 42 | (re-matches #"^commit ([^\s]+) .*") second)] 43 | [(git-branch-attached? repo) 44 | (git-branch-current repo) 45 | (do (git-checkout repo sha), 46 | (count (git-branch-current repo))) ; char count suggests sha 47 | (git-branch-attached? repo)])))))) -------------------------------------------------------------------------------- /src/clj_jgit/internal.clj: -------------------------------------------------------------------------------- 1 | (ns clj-jgit.internal 2 | (:import 3 | [org.eclipse.jgit.revwalk RevWalk RevCommit RevCommitList] 4 | [org.eclipse.jgit.treewalk TreeWalk] 5 | [org.eclipse.jgit.lib ObjectId ObjectIdRef Repository] 6 | [org.eclipse.jgit.api Git] 7 | [org.eclipse.jgit.transport RefSpec])) 8 | 9 | (defn ref-spec 10 | ^org.eclipse.jgit.transport.RefSpec [str] 11 | (RefSpec. str)) 12 | 13 | (defn new-rev-walk 14 | "Creates a new RevWalk instance (mutable)" 15 | ^org.eclipse.jgit.revwalk.RevWalk [^Git repo] 16 | (RevWalk. (.getRepository repo))) 17 | 18 | (defn new-tree-walk 19 | "Create new recursive TreeWalk instance (mutable)" 20 | ^org.eclipse.jgit.treewalk.TreeWalk [^Git repo ^RevCommit rev-commit] 21 | (doto 22 | (TreeWalk. (.getRepository repo)) 23 | (.addTree (.getTree rev-commit)) 24 | (.setRecursive true))) 25 | 26 | (defn bound-commit 27 | "Find a RevCommit object in a RevWalk and bound to it." 28 | ^org.eclipse.jgit.revwalk.RevCommit [^Git repo ^RevWalk rev-walk ^ObjectId rev-commit] 29 | (.parseCommit rev-walk rev-commit)) 30 | 31 | (defprotocol Resolvable 32 | "Protocol for things that resolve ObjectId's." 33 | (resolve-object [commit-ish repo] 34 | "Find ObjectId instance for any Git name: commit-ish, tree-ish or blob. Accepts ObjectId instances and just passes them through.")) 35 | 36 | (extend-type String 37 | Resolvable 38 | (resolve-object 39 | ^org.eclipse.jgit.lib.ObjectId [^String commit-ish ^Git repo] 40 | (.resolve (.getRepository repo) commit-ish))) 41 | 42 | (extend-type ObjectId 43 | Resolvable 44 | (resolve-object 45 | ^org.eclipse.jgit.lib.ObjectId [commit-ish ^Git repo] 46 | commit-ish)) 47 | 48 | (extend-type ObjectIdRef 49 | Resolvable 50 | (resolve-object 51 | ^org.eclipse.jgit.lib.ObjectId [commit-ish ^Git repo] 52 | (.getObjectId commit-ish))) 53 | 54 | (extend-type Git 55 | Resolvable 56 | (resolve-object 57 | ^org.eclipse.jgit.lib.ObjectId [^Git repo commit-ish] 58 | "For compatibility with previous implementation of resolve-object, which would take repo as a first argument." 59 | (resolve-object commit-ish repo))) 60 | 61 | (defn ref-database 62 | ^org.eclipse.jgit.lib.RefDatabase [^Git repo] 63 | (.getRefDatabase ^Repository (.getRepository repo))) 64 | 65 | (defn get-refs 66 | [^Git repo ^String prefix] 67 | (.getRefs (ref-database repo) prefix)) 68 | -------------------------------------------------------------------------------- /test/clj_jgit/test/querying.clj: -------------------------------------------------------------------------------- 1 | (ns clj-jgit.test.querying 2 | (:use 3 | [clj-jgit.test.helpers] 4 | [clj-jgit.internal] 5 | [clj-jgit.porcelain] 6 | [clj-jgit.querying] 7 | [clojure.test]) 8 | (:import 9 | [java.io File] 10 | [org.eclipse.jgit.api Git] 11 | [org.eclipse.jgit.lib Repository AnyObjectId ObjectId] 12 | [org.eclipse.jgit.revwalk RevWalk RevCommit] 13 | [org.eclipse.jgit.internal.storage.file RefDirectory$LooseUnpeeled])) 14 | 15 | (deftest querying-tests 16 | (testing "branch-list-with-heads" 17 | (read-only-repo 18 | (let [branches (branch-list-with-heads repo (new-rev-walk repo)) 19 | [branch-ref head-rev] (first branches)] 20 | (is (seq? branches)) 21 | (is (instance? RefDirectory$LooseUnpeeled branch-ref)) 22 | (is (instance? RevCommit head-rev))))) 23 | 24 | (testing "branches-for" 25 | (read-only-repo 26 | (let [first-commit (resolve-object "38dd57264cf5c05fb77211c8347d1f16e4474623" repo)] 27 | (is (some #(= % "refs/heads/master") (branches-for repo first-commit)))))) 28 | 29 | (testing "changed-files" 30 | (read-only-repo 31 | (are 32 | [commit-ish changes] (= changes (changes-for repo commit-ish)) 33 | "38dd57264cf5c05fb77211c8347d1f16e4474623" [[".gitignore" :add] 34 | ["README.md" :add] 35 | ["project.clj" :add] 36 | ["src/clj_jgit/core.clj" :add] 37 | ["src/clj_jgit/util/print.clj" :add] 38 | ["test/clj_jgit/test/core.clj" :add]] 39 | "edb462cd4ea2f351c4c5f20ec0952e70e113c489" [["src/clj_jgit/porcelain.clj" :edit] 40 | ["src/clj_jgit/util.clj" :add] 41 | ["src/clj_jgit/util/core.clj" :delete] 42 | ["src/clj_jgit/util/print.clj" :delete] 43 | ["test/clj_jgit/test/core.clj" :edit]] 44 | "0d3d1c2e7b6c47f901fcae9ef661a22948c64573" [[".gitignore" :edit] 45 | ["src/clj_jgit/porcelain.clj" :edit] 46 | ["src/clj_jgit/util.clj" :add] 47 | ["src/clj_jgit/util/core.clj" :delete] 48 | ["src/clj_jgit/util/print.clj" :delete] 49 | ["test/clj_jgit/test/core.clj" :edit] 50 | ["test/clj_jgit/test/porcelain.clj" :add]]))) 51 | 52 | (testing "rev-list" 53 | (read-only-repo 54 | (is (>= (count (rev-list repo (new-rev-walk repo))) 24)))) 55 | 56 | (testing "find-rev-commit" 57 | (read-only-repo 58 | (are [commit-ish] (instance? RevCommit (find-rev-commit repo (new-rev-walk repo) commit-ish)) 59 | "master" 60 | "38dd57264cf5c05fb77211c8347d1f16e4474623" 61 | "master^"))) 62 | 63 | (testing "commit-info" 64 | (read-only-repo 65 | (are 66 | [commit-ish info] (let [raw-data (-> commit-ish 67 | ((partial find-rev-commit repo (new-rev-walk repo))) 68 | ((partial commit-info repo))) 69 | expected-basic (dissoc raw-data :repo :raw :time :branches) 70 | info-basic (dissoc info :branches) 71 | target-branch (-> info :branches (first))] 72 | (and (= expected-basic info-basic) 73 | (some #(= % target-branch) (:branches raw-data)))) 74 | "38dd57264cf5c05fb77211c8347d1f16e4474623" {:changed_files 75 | [[".gitignore" :add] 76 | ["README.md" :add] 77 | ["project.clj" :add] 78 | ["src/clj_jgit/core.clj" :add] 79 | ["src/clj_jgit/util/print.clj" :add] 80 | ["test/clj_jgit/test/core.clj" :add]], 81 | :author "Daniel Gregoire", 82 | :email "daniel.l.gregoire@gmail.com", 83 | :message "Initial commit", 84 | :branches ["refs/heads/master"], 85 | :merge false, 86 | :id "38dd57264cf5c05fb77211c8347d1f16e4474623"} 87 | "edb462cd4ea2f351c4c5f20ec0952e70e113c489" {:changed_files 88 | [["src/clj_jgit/porcelain.clj" :edit] 89 | ["src/clj_jgit/util.clj" :add] 90 | ["src/clj_jgit/util/core.clj" :delete] 91 | ["src/clj_jgit/util/print.clj" :delete] 92 | ["test/clj_jgit/test/core.clj" :edit]], 93 | :author "vijaykiran", 94 | :email "mail@vijaykiran.com", 95 | :message "Utils - move into a single file.\n- Test for utils method.", 96 | :branches ["refs/heads/master"], 97 | :merge false, 98 | :id "edb462cd4ea2f351c4c5f20ec0952e70e113c489"} 99 | "0d3d1c2e7b6c47f901fcae9ef661a22948c64573" {:changed_files 100 | [[".gitignore" :edit] 101 | ["src/clj_jgit/porcelain.clj" :edit] 102 | ["src/clj_jgit/util.clj" :add] 103 | ["src/clj_jgit/util/core.clj" :delete] 104 | ["src/clj_jgit/util/print.clj" :delete] 105 | ["test/clj_jgit/test/core.clj" :edit] 106 | ["test/clj_jgit/test/porcelain.clj" :add]], 107 | :author "Daniel Gregoire", 108 | :email "daniel.l.gregoire@gmail.com", 109 | :message "Merge pull request #2 from vijaykiran/master\n\nInit Tests", 110 | :branches ["refs/heads/master"], 111 | :merge true, 112 | :id "0d3d1c2e7b6c47f901fcae9ef661a22948c64573"}))) 113 | 114 | (testing "changes-for should return nil on invalid commits" 115 | (is (nil? 116 | (with-tmp-repo "target/tmp" 117 | (let [tmp-file "target/tmp/tmp.txt"] 118 | (spit tmp-file "1") 119 | (git-add repo tmp-file) 120 | (git-commit repo "first commit") 121 | (changes-for repo "invalid")))))) 122 | 123 | (testing "find-rev-commit should return nil on invalid commits" 124 | (is (nil? 125 | (with-tmp-repo "target/tmp" 126 | (let [tmp-file "target/tmp/tmp.txt"] 127 | (spit tmp-file "1") 128 | (git-add repo tmp-file) 129 | (git-commit repo "first commit") 130 | (find-rev-commit repo (new-rev-walk repo) "invalid"))))))) 131 | -------------------------------------------------------------------------------- /src/clj_jgit/querying.clj: -------------------------------------------------------------------------------- 1 | (ns clj-jgit.querying 2 | (:require [clojure.java.io :as io] 3 | [clojure.string :as string] 4 | [clj-jgit.util :as util] 5 | [clj-jgit.porcelain :as porcelain] 6 | [clj-jgit.internal :refer :all]) 7 | (:import [org.eclipse.jgit.diff DiffFormatter DiffEntry] 8 | [org.eclipse.jgit.util.io DisabledOutputStream] 9 | [org.eclipse.jgit.diff RawTextComparator] 10 | [org.eclipse.jgit.revwalk RevWalk RevCommit RevCommitList] 11 | [org.eclipse.jgit.lib FileMode Repository ObjectIdRef ObjectId AnyObjectId Ref] 12 | [org.eclipse.jgit.api Git LogCommand] 13 | [org.eclipse.jgit.internal.storage.file RefDirectory$LooseRef] 14 | [java.util HashMap Date] 15 | [java.io ByteArrayOutputStream])) 16 | 17 | (declare change-kind create-tree-walk diff-formatter-for-changes 18 | byte-array-diff-formatter-for-changes changed-files-in-first-commit 19 | parse-diff-entry mark-all-heads-as-start-for!) 20 | 21 | (defn find-rev-commit 22 | "Find RevCommit instance in RevWalk by commit-ish. Returns nil if the commit is not found." 23 | [^Git repo ^RevWalk rev-walk commit-ish] 24 | (let [object (resolve-object commit-ish repo)] 25 | (if (nil? object) 26 | nil 27 | (bound-commit repo rev-walk object)))) 28 | 29 | (defn branch-list-with-heads 30 | "List of branches for a repo in pairs of [branch-ref branch-tip-commit]" 31 | ([^Git repo] 32 | (branch-list-with-heads repo (new-rev-walk repo))) 33 | ([^Git repo ^RevWalk rev-walk] 34 | (letfn [(zip-commits [^ObjectIdRef branch-ref] 35 | [branch-ref (bound-commit repo rev-walk (.getObjectId branch-ref))])] 36 | (let [branches (porcelain/git-branch-list repo)] 37 | (doall (map zip-commits branches)))))) 38 | 39 | (defn commit-in-branch? 40 | "Checks if commit is merged into branch" 41 | [^Git repo ^RevWalk rev-walk ^RevCommit branch-tip-commit ^ObjectId bound-commit] 42 | (.isMergedInto rev-walk bound-commit branch-tip-commit)) 43 | 44 | (defn branches-for 45 | "List of branches in which specific commit is present" 46 | [^Git repo ^ObjectId rev-commit] 47 | (let [rev-walk (new-rev-walk repo) 48 | bound-commit (bound-commit repo rev-walk rev-commit) 49 | branch-list (branch-list-with-heads repo rev-walk)] 50 | (->> 51 | (for [[^ObjectIdRef branch-ref ^RevCommit branch-tip-commit] branch-list 52 | :when branch-tip-commit] 53 | (do 54 | (when (commit-in-branch? repo rev-walk branch-tip-commit bound-commit) 55 | (.getName branch-ref)))) 56 | (remove nil?) 57 | doall))) 58 | 59 | (defn changed-files-between-commits 60 | "List of files changed between two RevCommit objects" 61 | [^Git repo ^RevCommit old-rev-commit ^RevCommit new-rev-commit] 62 | (let [df ^DiffFormatter (diff-formatter-for-changes repo) 63 | entries (.scan df old-rev-commit new-rev-commit)] 64 | (map parse-diff-entry entries))) 65 | 66 | (defn changed-files 67 | "List of files changed in RevCommit object" 68 | [^Git repo ^RevCommit rev-commit] 69 | (if-let [parent (first (.getParents rev-commit))] 70 | (changed-files-between-commits repo parent rev-commit) 71 | (changed-files-in-first-commit repo rev-commit))) 72 | 73 | (defn changed-files-with-patch 74 | "Patch with diff of all changes in RevCommit object" 75 | [^Git repo ^RevCommit rev-commit] 76 | (if-let [parent (first (.getParents rev-commit))] 77 | (let [rev-parent ^RevCommit parent 78 | out ^ByteArrayOutputStream (new ByteArrayOutputStream) 79 | df ^DiffFormatter (byte-array-diff-formatter-for-changes repo out)] 80 | (.format df rev-parent rev-commit) 81 | (.toString out)))) 82 | 83 | (defn changes-for 84 | "Find changes for commit-ish. Returns nil if the commit is not found." 85 | [^Git repo commit-ish] 86 | (let [rev-commit (->> commit-ish 87 | (find-rev-commit repo (new-rev-walk repo)))] 88 | (if (nil? rev-commit) 89 | nil 90 | (changed-files repo rev-commit)))) 91 | 92 | (defn rev-list 93 | "List of all revision in repo" 94 | ([^Git repo] 95 | (rev-list repo (new-rev-walk repo))) 96 | ([^Git repo ^RevWalk rev-walk] 97 | (.reset rev-walk) 98 | (mark-all-heads-as-start-for! repo rev-walk) 99 | (doto (RevCommitList.) 100 | (.source rev-walk) 101 | (.fillTo Integer/MAX_VALUE)))) 102 | 103 | (defn commit-info-without-branches 104 | [^Git repo ^RevWalk rev-walk ^RevCommit rev-commit] 105 | (let [ident (.getAuthorIdent rev-commit) 106 | time (-> (.getCommitTime rev-commit) (* 1000) Date.) 107 | message (-> (.getFullMessage rev-commit) str string/trim)] 108 | {:id (.getName rev-commit) 109 | :repo repo 110 | :author (.getName ident) 111 | :email (.getEmailAddress ident) 112 | :time time 113 | :message message 114 | :changed_files (changed-files repo rev-commit) 115 | :merge (> (.getParentCount rev-commit) 1) 116 | :raw rev-commit ; can't retain commit because then RevCommit can't be garbage collected 117 | })) 118 | 119 | (defn commit-info 120 | ([^Git repo, ^RevCommit rev-commit] 121 | (commit-info repo (new-rev-walk repo) rev-commit)) 122 | ([^Git repo, ^RevWalk rev-walk, ^RevCommit rev-commit] 123 | (merge (commit-info-without-branches repo rev-walk rev-commit) 124 | {:branches (branches-for repo rev-commit)})) 125 | ([^Git repo ^RevWalk rev-walk ^HashMap commit-map ^RevCommit rev-commit] 126 | (merge (commit-info-without-branches repo rev-walk rev-commit) 127 | {:branches (map #(.getName ^Ref %) (or (.get commit-map rev-commit) []))}))) 128 | 129 | (defn- mark-all-heads-as-start-for! 130 | [^Git repo ^RevWalk rev-walk] 131 | (doseq [[objId ref] (.getAllRefsByPeeledObjectId (.getRepository repo))] 132 | (.markStart rev-walk (.lookupCommit rev-walk objId)))) 133 | 134 | (defn- change-kind 135 | [^DiffEntry entry] 136 | (let [change (.. entry getChangeType name)] 137 | (cond 138 | (= change "ADD") :add 139 | (= change "MODIFY") :edit 140 | (= change "DELETE") :delete 141 | (= change "COPY") :copy))) 142 | 143 | (defn- diff-formatter-for-changes 144 | [^Git repo] 145 | (doto 146 | (DiffFormatter. DisabledOutputStream/INSTANCE) 147 | (.setRepository (.getRepository repo)) 148 | (.setDiffComparator RawTextComparator/DEFAULT) 149 | (.setDetectRenames false))) 150 | 151 | (defn- byte-array-diff-formatter-for-changes 152 | [^Git repo ^ByteArrayOutputStream out] 153 | (doto 154 | (new DiffFormatter out) 155 | (.setRepository (.getRepository repo)) 156 | (.setDiffComparator RawTextComparator/DEFAULT))) 157 | 158 | (defn- changed-files-in-first-commit 159 | [^Git repo ^RevCommit rev-commit] 160 | (let [tree-walk (new-tree-walk repo rev-commit) 161 | changes (transient [])] 162 | (while (.next tree-walk) 163 | (conj! changes [(util/normalize-path (.getPathString tree-walk)) :add])) 164 | (persistent! changes))) 165 | 166 | (defn- parse-diff-entry 167 | [^DiffEntry entry] 168 | (let [old-path (util/normalize-path (.getOldPath entry)) 169 | new-path (util/normalize-path (.getNewPath entry)) 170 | change-kind (change-kind entry)] 171 | (cond 172 | (= old-path new-path) [new-path change-kind] 173 | (= old-path "dev/null") [new-path change-kind] 174 | (= new-path "dev/null") [old-path change-kind] 175 | :else [old-path change-kind new-path]))) 176 | 177 | (defn rev-list-for 178 | ([^Git repo ^RevWalk rev-walk ^RefDirectory$LooseRef object] 179 | (.reset rev-walk) 180 | (.markStart rev-walk (.lookupCommit ^RevWalk rev-walk ^AnyObjectId (.getObjectId object))) 181 | (.toArray 182 | (doto (RevCommitList.) 183 | (.source rev-walk) 184 | (.fillTo Integer/MAX_VALUE))))) 185 | 186 | (defn- add-branch-to-map 187 | [^Git repo ^RevWalk rev-walk branch ^HashMap m] 188 | (let [^"[Ljava.lang.Object;" revs (rev-list-for repo rev-walk branch)] 189 | (dotimes [i (alength revs)] 190 | (let [c (aget revs i)] 191 | (.put m c (conj (or (.get m c) []) branch)))))) 192 | 193 | (defn build-commit-map 194 | "Build commit map, which is a map of commit IDs to the list of branches they are in." 195 | ([repo] 196 | (build-commit-map repo (new-rev-walk repo))) 197 | ([^Git repo ^RevWalk rev-walk] 198 | (let [^HashMap m (HashMap.)] 199 | (loop [[branch & branches] (vals (get-refs repo "refs/heads/"))] 200 | (add-branch-to-map repo rev-walk branch m) 201 | (if branches 202 | (recur branches) 203 | m))))) 204 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # clj-jgit 2 | 3 | Clojure wrapper for using the JGit library to manipulate Git repositories in a "pure Java" fashion. 4 | 5 | You can view latest auto-generated API documentation here: [clj-jgit.github.io/clj-jgit](http://clj-jgit.github.io/clj-jgit). 6 | 7 | ## Installation ## 8 | 9 | Last stable version is available on [Clojars](http://clojars.org/clj-jgit). 10 | 11 | [![Clojars Project](http://clojars.org/clj-jgit/latest-version.svg)](http://clojars.org/clj-jgit) 12 | 13 | ## Quickstart Tutorial ## 14 | 15 | This brief tutorial will show you how to: 16 | 17 | 1. Clone a remote repository 18 | 2. Create a local branch for your changes 19 | 3. Checkout that branch, and 20 | 4. Add and commit those changes 21 | 22 | ```clj 23 | ;; Clone a repository into a folder of my choosing 24 | (def my-repo 25 | (git-clone-full "https://github.com/clj-jgit/clj-jgit.git" "local-folder/clj-jgit") 26 | ;=> # 27 | 28 | ;; A bit redundant for a fresh repo, but always good to check the repo status 29 | ;; before making any changes 30 | (git-status my-repo) 31 | ;=> {:untracked #{}, :removed #{}, :modified #{}, :missing #{}, :changed #{}, :added #{}} 32 | 33 | ;; List existing branches 34 | (git-branch-list my-repo) 35 | ;=> (#) 36 | 37 | ;; Create a new local branch to store our changes 38 | (git-branch-create my-repo "my-branch") 39 | ;=> # 40 | 41 | ;; Prove to ourselves that it was created 42 | (git-branch-list my-repo) 43 | ;=> (# #) 44 | 45 | ;; Check out our new branch 46 | 47 | (git-checkout my-repo "my-branch") 48 | ;=> # 49 | 50 | ;; Now go off and make your changes. 51 | ;; For example, let's say we added a file "foo.txt" at the base of the project. 52 | (git-status my-repo) 53 | ;=> {:untracked #{"foo.txt"}, :removed #{}, :modified #{}, :missing #{}, :changed #{}, :added #{}} 54 | 55 | ;; Add the file to the index 56 | (git-add my-repo "foo.txt") 57 | ;=> # 58 | 59 | ;; Check for status change 60 | (git-status my-repo) 61 | ;=> {:untracked #{}, :removed #{}, :modified #{}, :missing #{}, :changed #{}, :added #{"foo.txt"}} 62 | 63 | ;; Now commit your changes, specifying author and committer if desired 64 | (git-commit my-repo "Add file foo.txt" {"Daniel Gregoire" "daniel@example.com"}) 65 | ;=> # 66 | 67 | ;; Status clean 68 | (git-status my-repo) 69 | ;=> {:untracked #{}, :removed #{}, :modified #{}, :missing #{}, :changed #{}, :added #{}} 70 | 71 | (git-clean my-repo :clean-dirs? true, :ignore? true) 72 | ;=> ... 73 | 74 | ;; Blame 75 | (first (git-blame my-repo "README.md")) 76 | ;=> {:author {:name "Ilya Sabanin", 77 | :email "ilya@sabanin.com", 78 | :timezone #}, 79 | :commit #, 80 | :committer {:name "Ilya Sabanin", 81 | :email "ilya@wildbit.com", 82 | :timezone #}, 83 | :line 66, 84 | :source-path "README.md"} 85 | 86 | ``` 87 | 88 | ## Detailed Usage ## 89 | 90 | Currently, this library leverages the "porcelain" API exposed by JGit, which allows the use of methods/functions similar to the ones available on the command-line when using Git for "basic" purposes. If enough interest arises, this tool will wrap more of the lower-level functions in the future, but for now it acts as a basic Java replacement for the command-line version of Git. 91 | 92 | ### Cloning a Repository ### 93 | 94 | ```clj 95 | (git-clone-full "url-to-read-only-repo" "optional-local-folder") 96 | ``` 97 | 98 | JGit's default `git-clone` simply clones the `.git` folder, but doesn't pull down the actual project files. This library's `git-clone-full` function, on the other hand, performs a `git-clone` following by a `git-fetch` of the master branch and a `git-merge`. 99 | 100 | ### Loading an Existing Repository ### 101 | 102 | In order to use most of the functions in JGit's API, you need to have a repository object to play with. Here are ways to load an existing repository: 103 | 104 | ```clj 105 | ;; Simples method is to point to the folder 106 | (load-repo "path-to-git-repo-folder") 107 | ;; In order to remain consistent with JGit's default behavior, 108 | ;; you can also point directly at the .git folder of the target repo 109 | (load-repo "path-to-git-repo-folder/.git") 110 | ``` 111 | 112 | This function throws a `FileNotFoundException` if the directory in question does not exist. 113 | 114 | ### Querying repository 115 | 116 | This uses internal JGit API, so it may require some additional knowledge of JGit. 117 | 118 | ```clj 119 | (ns clj-jgit.porcelain) 120 | 121 | ;; Log 122 | (git-log my-repo) 123 | ;=> (# ...) 124 | 125 | ;; Log for range 126 | (git-log my-repo "36748f70" "master^3") 127 | ;=> (# ...) 128 | ``` 129 | 130 | ```clj 131 | ;; This macro allows you to create a universal handler with name "repo" 132 | (with-repo "/path/to/a/repo" 133 | (git-log repo)) 134 | ``` 135 | 136 | ```clj 137 | (ns clj-jgit.querying) 138 | 139 | ;; Creates a RevWalk instance needed to traverse the commits for the repo. 140 | ;; Commits found through a RevWalk can be compared and used only with other 141 | ;; commits found with a same RevWalk instance. 142 | (def rev-walk (new-rev-walk repo)) 143 | 144 | ;; List of pairs of branch refs and RevCommits associated with them 145 | (branch-list-with-heads repo rev-walk) 146 | ;=> ([# 147 | #]) 148 | 149 | ;; Find an ObjectId instance for a repo and given commit-ish, tree-ish or blob 150 | (def commit-obj-id (resolve-object repo "38dd57264cf5c05fb77211c8347d1f16e4474623")) 151 | ;=> # 152 | 153 | ;; Show all the branches where commit is present 154 | (branches-for repo commit-obj-id) 155 | ;=> ("refs/heads/master") 156 | 157 | ;; List of all revision objects in the repository, for all branches 158 | (rev-list repo) 159 | ;=> # 160 | ``` 161 | 162 | ```clj 163 | ;; Gather information about specific commit 164 | (commit-info repo (find-rev-commit repo rev-walk "38dd57264cf")) 165 | 166 | ; Returns 167 | {:repo #, 168 | :changed_files [[".gitignore" :add] 169 | ["README.md" :add] 170 | ["project.clj" :add] 171 | ["src/clj_jgit/core.clj" :add] 172 | ["src/clj_jgit/util/print.clj" :add] 173 | ["test/clj_jgit/test/core.clj" :add]], 174 | :raw #, 175 | :author "Daniel Gregoire", 176 | :email "daniel.l.gregoire@gmail.com", 177 | :message "Initial commit", 178 | :branches ("refs/heads/master"), 179 | :merge false, 180 | :time #, 181 | :id "38dd57264cf5c05fb77211c8347d1f16e4474623"} 182 | 183 | ;; You can also combine this with Porcelain API, to get a list of all commits in a repo with detailed information 184 | (with-repo "/path/to/repo.git" 185 | (map #(commit-info repo %) (git-log repo))) 186 | 187 | ;; Branches lookup is an expensive operation, especially for repos with many branches. 188 | ;; commit-info spends most of it time trying to detect list of branches commit belongs to. 189 | 190 | ; If you don't require branches list in commit info, you can use: 191 | (commit-info-without-branches repo rev-walk rev-commit) 192 | 193 | ; If you want branches list, but want it to work faster, you can generate commit map that turns 194 | ; commits and branches into a map for fast branch lookups. In real-life this can give 30x-100x speed 195 | ; up when you are traversing lists of commits, depending on amount of branches you have. 196 | (let [rev-walk (new-rev-walk repo) 197 | commit-map (build-commit-map repo rev-walk) 198 | commits (git-log repo)] 199 | (map (partial commit-info repo rev-walk commit-map) commits)) 200 | ``` 201 | ### Contribute ### 202 | 203 | If you want to contribute just fork the repository, work on the code, cover it with tests and submit a pull request through Github. 204 | 205 | Any questions related to clj-jgit can be discussed in the [Google Group](https://groups.google.com/forum/#!forum/clj-jgit). 206 | 207 | ## Caveat Windows Users 208 | 209 | Cygwin will cause this library to hang. Make sure to remove `C:\cygwin\bin` from your PATH before attempting to use this library. 210 | 211 | ## License 212 | 213 | Copyright (C) 2011 FIXME 214 | 215 | Distributed under the Eclipse Public License, the same as Clojure. 216 | -------------------------------------------------------------------------------- /src/clj_jgit/porcelain.clj: -------------------------------------------------------------------------------- 1 | (ns clj-jgit.porcelain 2 | (:require [clojure.java.io :as io] 3 | [clojure.string :as str] 4 | [clj-jgit.util :as util] 5 | [clj-jgit.internal :refer :all] 6 | [fs.core :as fs]) 7 | (:import [java.io FileNotFoundException File] 8 | [org.eclipse.jgit.lib RepositoryBuilder AnyObjectId] 9 | [org.eclipse.jgit.api Git InitCommand StatusCommand AddCommand 10 | ListBranchCommand PullCommand MergeCommand LogCommand 11 | LsRemoteCommand Status ResetCommand$ResetType 12 | FetchCommand] 13 | [org.eclipse.jgit.submodule SubmoduleWalk] 14 | [com.jcraft.jsch Session JSch] 15 | [org.eclipse.jgit.transport FetchResult JschConfigSessionFactory 16 | OpenSshConfig$Host SshSessionFactory] 17 | [org.eclipse.jgit.util FS] 18 | [org.eclipse.jgit.merge MergeStrategy] 19 | [clojure.lang Keyword] 20 | [java.util List] 21 | [org.eclipse.jgit.api.errors JGitInternalException] 22 | [org.eclipse.jgit.transport UsernamePasswordCredentialsProvider] 23 | [org.eclipse.jgit.treewalk TreeWalk])) 24 | 25 | (declare log-builder) 26 | 27 | (defmulti discover-repo "Discover a Git repository in a path." type) 28 | 29 | (defmethod discover-repo File 30 | [^File file] 31 | (discover-repo (.getPath file))) 32 | 33 | (defmethod discover-repo String 34 | [^String path] 35 | (let [with-git (io/as-file (str path "/.git")) 36 | bare (io/as-file (str path "/refs"))] 37 | (cond 38 | (.endsWith path ".git") (io/as-file path) 39 | (.exists with-git) with-git 40 | (.exists bare) (io/as-file path)))) 41 | 42 | (def ^:dynamic *credentials* nil) 43 | (def ^:dynamic *ssh-identity-name* "") 44 | (def ^:dynamic *ssh-prvkey* nil) 45 | (def ^:dynamic *ssh-pubkey* nil) 46 | (def ^:dynamic *ssh-passphrase* "") 47 | (def ^:dynamic *ssh-identities* []) 48 | (def ^:dynamic *ssh-exclusive-identity* false) 49 | (def ^:dynamic *ssh-session-config* {"StrictHostKeyChecking" "no" 50 | "UserKnownHostsFile" "/dev/null"}) 51 | 52 | (defmacro with-credentials 53 | [login password & body] 54 | `(binding [*credentials* (UsernamePasswordCredentialsProvider. ~login ~password)] 55 | ~@body)) 56 | 57 | (defn load-repo 58 | "Given a path (either to the parent folder or to the `.git` folder itself), load the Git repository" 59 | ^org.eclipse.jgit.api.Git [path] 60 | (if-let [git-dir (discover-repo path)] 61 | (-> (RepositoryBuilder.) 62 | (.setGitDir git-dir) 63 | (.readEnvironment) 64 | (.findGitDir) 65 | (.build) 66 | (Git.)) 67 | (throw 68 | (FileNotFoundException. (str "The Git repository at '" path "' could not be located."))))) 69 | 70 | (defmacro with-repo 71 | "Binds `repo` to a repository handle" 72 | [path & body] 73 | `(let [~'repo (load-repo ~path) 74 | ~'rev-walk (new-rev-walk ~'repo)] 75 | ~@body)) 76 | 77 | (defn git-add 78 | "The `file-pattern` is either a single file name (exact, not a pattern) or the name of a directory. If a directory is supplied, all files within that directory will be added. If `only-update?` is set to `true`, only files which are already part of the index will have their changes staged (i.e. no previously untracked files will be added to the index)." 79 | ([^Git repo file-pattern] 80 | (git-add repo file-pattern false nil)) 81 | ([^Git repo file-pattern only-update?] 82 | (git-add repo file-pattern only-update? nil)) 83 | ([^Git repo file-pattern only-update? working-tree-iterator] 84 | (-> repo 85 | (.add) 86 | (.addFilepattern file-pattern) 87 | (.setUpdate only-update?) 88 | (.setWorkingTreeIterator working-tree-iterator) 89 | (.call)))) 90 | 91 | (defn git-branch-list 92 | "Get a list of branches in the Git repo. Return the default objects generated by the JGit API." 93 | ([^Git repo] 94 | (git-branch-list repo :local)) 95 | ([^Git repo opt] 96 | (let [opt-val {:all org.eclipse.jgit.api.ListBranchCommand$ListMode/ALL 97 | :remote org.eclipse.jgit.api.ListBranchCommand$ListMode/REMOTE} 98 | branches (if (= opt :local) 99 | (-> repo 100 | (.branchList) 101 | (.call)) 102 | (-> repo 103 | (.branchList) 104 | (.setListMode (opt opt-val)) 105 | (.call)))] 106 | (seq branches)))) 107 | 108 | (defn git-branch-current* 109 | [^Git repo] 110 | (.getFullBranch (.getRepository repo))) 111 | 112 | (defn git-branch-current 113 | "The current branch of the git repo" 114 | [^Git repo] 115 | (str/replace (git-branch-current* repo) #"^refs/heads/" "")) 116 | 117 | (defn git-branch-attached? 118 | "Is the current repo on a branch (true) or in a detached HEAD state?" 119 | [^Git repo] 120 | (not (nil? (re-find #"^refs/heads/" (git-branch-current* repo))))) 121 | 122 | (defn git-branch-create 123 | "Create a new branch in the Git repository." 124 | ([^Git repo branch-name] 125 | (git-branch-create repo branch-name false nil)) 126 | ([^Git repo branch-name force?] 127 | (git-branch-create repo branch-name force? nil)) 128 | ([^Git repo branch-name force? ^String start-point] 129 | (if (nil? start-point) 130 | (-> repo 131 | (.branchCreate) 132 | (.setName branch-name) 133 | (.setForce force?) 134 | (.call)) 135 | (-> repo 136 | (.branchCreate) 137 | (.setName branch-name) 138 | (.setForce force?) 139 | (.setStartPoint start-point) 140 | (.call))))) 141 | 142 | (defn git-branch-delete 143 | ([^Git repo branch-names] 144 | (git-branch-delete repo branch-names false)) 145 | ([^Git repo branch-names force?] 146 | (-> repo 147 | (.branchDelete) 148 | (.setBranchNames (into-array String branch-names)) 149 | (.setForce force?) 150 | (.call)))) 151 | 152 | (defn git-checkout 153 | ([^Git repo branch-name] 154 | (git-checkout repo branch-name false false nil)) 155 | ([^Git repo branch-name create-branch?] 156 | (git-checkout repo branch-name create-branch? false nil)) 157 | ([^Git repo branch-name create-branch? force?] 158 | (git-checkout repo branch-name create-branch? force? nil)) 159 | ([^Git repo branch-name create-branch? force? ^String start-point] 160 | (if (nil? start-point) 161 | (-> repo 162 | (.checkout) 163 | (.setName branch-name) 164 | (.setCreateBranch create-branch?) 165 | (.setForce force?) 166 | (.call)) 167 | (-> repo 168 | (.checkout) 169 | (.setName branch-name) 170 | (.setCreateBranch create-branch?) 171 | (.setForce force?) 172 | (.setStartPoint start-point) 173 | (.call))))) 174 | 175 | (declare git-cherry-pick) 176 | 177 | (defn clone-cmd [uri] 178 | (-> (Git/cloneRepository) 179 | (.setCredentialsProvider *credentials*) 180 | (.setURI uri))) 181 | 182 | (defn git-clone 183 | ([uri] 184 | (git-clone uri (util/name-from-uri uri) "origin" "master" false)) 185 | ([uri local-dir] 186 | (git-clone uri local-dir "origin" "master" false)) 187 | ([uri local-dir remote-name] 188 | (git-clone uri local-dir remote-name "master" false)) 189 | ([uri local-dir remote-name local-branch] 190 | (git-clone uri local-dir remote-name local-branch false)) 191 | ([uri local-dir remote-name local-branch bare?] 192 | (-> (clone-cmd uri) 193 | (.setDirectory (io/as-file local-dir)) 194 | (.setRemote remote-name) 195 | (.setBranch local-branch) 196 | (.setBare bare?) 197 | (.call)))) 198 | 199 | (defn git-clone2 200 | [uri {:as options 201 | :keys [path remote-name branch-name bare clone-all-branches] 202 | :or {path (util/name-from-uri uri) 203 | remote-name "origin" 204 | branch-name "master" 205 | bare false 206 | clone-all-branches true}}] 207 | (doto (clone-cmd uri) 208 | (.setDirectory (io/as-file path)) 209 | (.setRemote remote-name) 210 | (.setBranch branch-name) 211 | (.setBare bare) 212 | (.setCloneAllBranches clone-all-branches) 213 | (.call))) 214 | 215 | (declare git-fetch git-merge) 216 | 217 | (defn git-clone-full 218 | "Clone, fetch the master branch and merge its latest commit" 219 | ([uri] 220 | (git-clone-full uri (util/name-from-uri uri) "origin" "master" false)) 221 | ([uri local-dir] 222 | (git-clone-full uri local-dir "origin" "master" false)) 223 | ([uri local-dir remote-name] 224 | (git-clone-full uri local-dir remote-name "master" false)) 225 | ([uri local-dir remote-name local-branch] 226 | (git-clone-full uri local-dir remote-name local-branch false)) 227 | ([uri local-dir remote-name local-branch bare?] 228 | (let [new-repo (-> (clone-cmd uri) 229 | (.setDirectory (io/as-file local-dir)) 230 | (.setRemote remote-name) 231 | (.setBranch local-branch) 232 | (.setBare bare?) 233 | (.call)) 234 | fetch-result ^FetchResult (git-fetch new-repo) 235 | merge-result (git-merge new-repo 236 | (first (.getAdvertisedRefs fetch-result)))] 237 | {:repo new-repo, 238 | :fetch-result fetch-result, 239 | :merge-result merge-result}))) 240 | 241 | (defn git-commit 242 | "Commit staged changes." 243 | ([^Git repo message] 244 | (-> repo 245 | (.commit) 246 | (.setMessage message) 247 | (.call))) 248 | ([^Git repo message {:keys [name email]}] 249 | (-> repo 250 | (.commit) 251 | (.setMessage message) 252 | (.setAuthor name email) 253 | (.setCommitter name email) 254 | (.call))) 255 | ([^Git repo message {:keys [author-name author-email]} {:keys [committer-name committer-email]}] 256 | (-> repo 257 | (.commit) 258 | (.setMessage message) 259 | (.setAuthor author-name author-email) 260 | (.setCommitter committer-name committer-email) 261 | (.call)))) 262 | 263 | (defn git-commit-amend 264 | "Amend previous commit with staged changes." 265 | ([^Git repo message] 266 | (-> repo 267 | (.commit) 268 | (.setMessage message) 269 | (.setAmend true) 270 | (.call))) 271 | ([^Git repo message {:keys [name email]}] 272 | (-> repo 273 | (.commit) 274 | (.setMessage message) 275 | (.setAuthor name email) 276 | (.setAmend true) 277 | (.call))) 278 | ([^Git repo message {:keys [name email]} {:keys [name email]}] 279 | (-> repo 280 | (.commit) 281 | (.setMessage message) 282 | (.setAuthor name email) 283 | (.setCommitter name email) 284 | (.setAmend true) 285 | (.call)))) 286 | 287 | 288 | (defn git-add-and-commit 289 | "This is the `git commit -a...` command" 290 | ([^Git repo message] 291 | (-> repo 292 | (.commit) 293 | (.setMessage message) 294 | (.setAll true) 295 | (.call))) 296 | ([^Git repo message {:keys [name email]}] 297 | (-> repo 298 | (.commit) 299 | (.setMessage message) 300 | (.setAuthor name email) 301 | (.setAll true) 302 | (.call))) 303 | ([^Git repo message {:keys [name email]} {:keys [name email]}] 304 | (-> repo 305 | (.commit) 306 | (.setMessage message) 307 | (.setAuthor name email) 308 | (.setCommitter name email) 309 | (.setAll true) 310 | (.call)))) 311 | 312 | (defn fetch-cmd [^Git repo] 313 | (-> repo 314 | (.fetch) 315 | (.setCredentialsProvider *credentials*))) 316 | 317 | (defn git-fetch 318 | "Fetch changes from upstream repository." 319 | (^org.eclipse.jgit.transport.FetchResult [^Git repo] 320 | (-> (fetch-cmd repo) 321 | (.call))) 322 | (^org.eclipse.jgit.transport.FetchResult [^Git repo remote] 323 | (-> (fetch-cmd repo) 324 | (.setRemote remote) 325 | (.call))) 326 | (^org.eclipse.jgit.transport.FetchResult [^Git repo remote & refspecs] 327 | (let [^FetchCommand cmd (fetch-cmd repo)] 328 | (.setRefSpecs cmd ^List (map ref-spec refspecs)) 329 | (.setRemote cmd remote) 330 | (.call cmd)))) 331 | 332 | (defn git-fetch-all 333 | "Fetch all refs from upstream repository" 334 | ([^Git repo] 335 | (git-fetch-all repo "origin")) 336 | ([^Git repo remote] 337 | (git-fetch repo remote "+refs/tags/*:refs/tags/*" "+refs/heads/*:refs/heads/*"))) 338 | 339 | (defn git-init 340 | "Initialize and load a new Git repository" 341 | ([] (git-init ".")) 342 | ([target-dir] 343 | (let [comm (InitCommand.)] 344 | (-> comm 345 | (.setDirectory (io/as-file target-dir)) 346 | (.call))))) 347 | 348 | (defn git-log 349 | "Return a seq of all commit objects" 350 | ([^Git repo] 351 | (seq (-> repo 352 | (.log) 353 | (.call)))) 354 | ([^Git repo hash] 355 | (seq (-> repo 356 | (.log) 357 | (.add (resolve-object hash repo)) 358 | (.call)))) 359 | ([^Git repo hash-a hash-b] 360 | (seq (-> repo 361 | ^LogCommand (log-builder hash-a hash-b) 362 | (.call))))) 363 | 364 | (defn- log-builder 365 | "Builds a log command object for a range of commit-ish names" 366 | ^org.eclipse.jgit.api.LogCommand [^Git repo hash-a hash-b] 367 | (let [log (.log repo)] 368 | (if (= hash-a "0000000000000000000000000000000000000000") 369 | (.add log (resolve-object hash-b repo)) 370 | (.addRange log (resolve-object hash-a repo) (resolve-object hash-b repo))))) 371 | 372 | (def merge-strategies 373 | {:ours MergeStrategy/OURS 374 | :resolve MergeStrategy/RESOLVE 375 | :simple-two-way MergeStrategy/SIMPLE_TWO_WAY_IN_CORE 376 | :theirs MergeStrategy/THEIRS}) 377 | 378 | (defn git-merge 379 | "Merge ref in current branch." 380 | ([^Git repo commit-ref] 381 | (let [commit-obj (resolve-object commit-ref repo)] 382 | (-> repo 383 | (.merge) 384 | ^MergeCommand (.include commit-obj) 385 | (.call)))) 386 | ([^Git repo commit-ref ^Keyword strategy] 387 | (let [commit-obj (resolve-object commit-ref repo) 388 | strategy-obj ^MergeStrategy (merge-strategies strategy)] 389 | (-> repo 390 | (.merge) 391 | ^MergeCommand (.include commit-obj) 392 | ^MergeCommand (.setStrategy strategy-obj) 393 | (.call))))) 394 | 395 | (defn git-pull 396 | "NOT WORKING: Requires work with configuration" 397 | [^Git repo] 398 | (-> repo 399 | (.pull) 400 | (.call))) 401 | 402 | (defn git-push []) 403 | (defn git-rebase []) 404 | (defn git-revert []) 405 | (defn git-rm 406 | [^Git repo file-pattern] 407 | (-> repo 408 | (.rm) 409 | (.addFilepattern file-pattern) 410 | (.call))) 411 | 412 | (defn git-status 413 | "Return the status of the Git repository. Opts will return individual aspects of the status, and can be specified as `:added`, `:changed`, `:missing`, `:modified`, `:removed`, or `:untracked`." 414 | [^Git repo & fields] 415 | (let [status (.. repo status call) 416 | status-fns {:added #(.getAdded ^Status %) 417 | :changed #(.getChanged ^Status %) 418 | :missing #(.getMissing ^Status %) 419 | :modified #(.getModified ^Status %) 420 | :removed #(.getRemoved ^Status %) 421 | :untracked #(.getUntracked ^Status %)}] 422 | (if-not (seq fields) 423 | (apply merge (for [[k f] status-fns] 424 | {k (into #{} (f status))})) 425 | (apply merge (for [field fields] 426 | {field (into #{} ((field status-fns) status))}))))) 427 | 428 | (defn git-tag []) 429 | 430 | (defn ls-remote-cmd [^Git repo] 431 | (-> repo 432 | (.lsRemote) 433 | (.setCredentialsProvider *credentials*))) 434 | 435 | (defn git-ls-remote 436 | ([^Git repo] 437 | (-> (ls-remote-cmd repo) 438 | (.call))) 439 | ([^Git repo remote] 440 | (-> (ls-remote-cmd repo) 441 | (.setRemote remote) 442 | (.call))) 443 | ([^Git repo remote opts] 444 | (-> (ls-remote-cmd repo) 445 | (.setRemote remote) 446 | (.setHeads (:heads opts false)) 447 | (.setTags (:tags opts false)) 448 | (.call)))) 449 | 450 | (def reset-modes 451 | {:hard ResetCommand$ResetType/HARD 452 | :keep ResetCommand$ResetType/KEEP 453 | :merge ResetCommand$ResetType/MERGE 454 | :mixed ResetCommand$ResetType/MIXED 455 | :soft ResetCommand$ResetType/SOFT}) 456 | 457 | (defn git-reset 458 | ([^Git repo ref] 459 | (git-reset repo ref :mixed)) 460 | ([^Git repo ref mode-sym] 461 | (-> repo .reset 462 | (.setRef ref) 463 | (.setMode ^ResetCommand$ResetType (reset-modes mode-sym)) 464 | (.call)))) 465 | 466 | (def jsch-factory 467 | (proxy [JschConfigSessionFactory] [] 468 | (configure [hc session] 469 | (let [jsch (.getJSch this hc FS/DETECTED)] 470 | (doseq [[key val] *ssh-session-config*] 471 | (.setConfig session key val)) 472 | (when *ssh-exclusive-identity* 473 | (.removeAllIdentity jsch)) 474 | (when (and *ssh-prvkey* *ssh-pubkey* *ssh-passphrase*) 475 | (.addIdentity jsch *ssh-identity-name* 476 | (.getBytes *ssh-prvkey* ) 477 | (.getBytes *ssh-pubkey*) 478 | (.getBytes *ssh-passphrase*))) 479 | (doseq [{:keys [name private-key public-key passphrase] 480 | :or {passphrase "" 481 | name (str "key-" (.hashCode private-key))}} *ssh-identities*] 482 | (.addIdentity jsch name 483 | (.getBytes private-key) 484 | (.getBytes public-key) 485 | (.getBytes passphrase))))))) 486 | 487 | (SshSessionFactory/setInstance jsch-factory) 488 | 489 | (defmacro with-identity 490 | "Creates an identity to use for SSH authentication." 491 | [config & body] 492 | `(let [name# (get ~config :name "jgit-identity") 493 | private# (get ~config :private) 494 | public# (get ~config :public) 495 | passphrase# (get ~config :passphrase "") 496 | options# (get ~config :options *ssh-session-config*) 497 | exclusive# (get ~config :exclusive false) 498 | identities# (get ~config :identities)] 499 | (binding [*ssh-identity-name* name# 500 | *ssh-prvkey* private# 501 | *ssh-pubkey* public# 502 | *ssh-identities* identities# 503 | *ssh-passphrase* passphrase# 504 | *ssh-session-config* options# 505 | *ssh-exclusive-identity* exclusive#] 506 | ~@body))) 507 | 508 | (defn submodule-walk 509 | ([repo] 510 | (->> (submodule-walk (.getRepository repo) 0) 511 | (flatten) 512 | (filter identity) 513 | (map #(Git/wrap %)))) 514 | ([repo level] 515 | (when (< level 3) 516 | (let [gen (SubmoduleWalk/forIndex repo) 517 | repos (transient [])] 518 | (while (.next gen) 519 | (when-let [subm (.getRepository gen)] 520 | (conj! repos subm) 521 | (conj! repos (submodule-walk subm (inc level))))) 522 | (->> (persistent! repos) 523 | (flatten)))))) 524 | 525 | (defn git-submodule-fetch 526 | [repo] 527 | (doseq [subm (submodule-walk repo)] 528 | (git-fetch-all subm))) 529 | 530 | (defn submodule-update-cmd [^Git repo] 531 | (-> repo 532 | (.submoduleUpdate) 533 | (.setCredentialsProvider *credentials*))) 534 | 535 | (defn git-submodule-update 536 | ([repo] 537 | "Fetch each submodule repo and update them." 538 | (git-submodule-fetch repo) 539 | (-> (submodule-update-cmd repo) 540 | (.call)) 541 | (doseq [subm (submodule-walk repo)] 542 | (-> (submodule-update-cmd subm) 543 | (.call)))) 544 | ([repo path] 545 | (git-submodule-fetch repo) 546 | (-> (submodule-update-cmd repo) 547 | (.call)) 548 | (doseq [subm (submodule-walk repo)] 549 | (-> (submodule-update-cmd subm) 550 | (.addPath path) 551 | (.call))))) 552 | 553 | (defn git-submodule-sync 554 | ([repo] 555 | (.. repo submoduleSync call) 556 | (doseq [subm (submodule-walk repo)] 557 | (.. subm submoduleSync call))) 558 | ([repo path] 559 | (.. repo submoduleSync call) 560 | (doseq [subm (submodule-walk repo)] 561 | (-> subm 562 | (.submoduleSync) 563 | (.addPath path) 564 | (.call))))) 565 | 566 | (defn git-submodule-init 567 | ([repo] 568 | (.. repo submoduleInit call) 569 | (doseq [subm (submodule-walk repo)] 570 | (.. subm submoduleInit call))) 571 | ([repo path] 572 | (.. repo submoduleInit call) 573 | (doseq [subm (submodule-walk repo)] 574 | (-> subm 575 | (.submoduleInit) 576 | (.addPath path) 577 | (.call))))) 578 | 579 | (defn git-submodule-add 580 | [repo uri path] 581 | (-> repo 582 | (.submoduleAdd) 583 | (.setURI uri) 584 | (.setPath path) 585 | (.call))) 586 | 587 | ;; 588 | ;; Git Stash Commands 589 | ;; 590 | 591 | (defn git-create-stash 592 | [^Git repo] 593 | (-> repo 594 | .stashCreate 595 | .call)) 596 | 597 | (defn git-apply-stash 598 | ([^Git repo] 599 | (git-apply-stash repo nil)) 600 | ([^Git repo ^String ref-id] 601 | (-> repo 602 | .stashApply 603 | (.setStashRef ref-id) 604 | .call))) 605 | 606 | (defn git-list-stash 607 | [^Git repo] 608 | (-> repo 609 | .stashList 610 | .call)) 611 | 612 | (defn git-drop-stash 613 | ([^Git repo] 614 | (-> repo 615 | .stashDrop 616 | .call)) 617 | ([^Git repo ^String ref-id] 618 | (let [stashes (git-list-stash repo) 619 | target (first (filter #(= ref-id (second %)) 620 | (map-indexed #(vector %1 (.getName %2)) stashes)))] 621 | (when-not (nil? target) 622 | (-> repo 623 | .stashDrop 624 | (.setStashRef (first target)) 625 | .call))))) 626 | 627 | (defn git-pop-stash 628 | ([^Git repo] 629 | (git-apply-stash repo) 630 | (git-drop-stash repo)) 631 | ([^Git repo ^String ref-id] 632 | (git-apply-stash repo ref-id) 633 | (git-drop-stash repo ref-id))) 634 | 635 | (defn git-clean 636 | "Remove untracked files from the working tree. 637 | 638 | clean-dirs? - true/false - remove untracked directories 639 | force-dirs? - true/false - force removal of non-empty untracked directories 640 | paths - set of paths to cleanup 641 | ignore? - true/false - ignore paths from .gitignore" 642 | [^Git repo & {:keys [clean-dirs? ignore? paths force-dirs?] 643 | :or {clean-dirs? false 644 | force-dirs? false 645 | ignore? true 646 | paths #{}}}] 647 | (letfn [(clean-loop [retries] 648 | (try 649 | (-> repo 650 | (.clean) 651 | (.setCleanDirectories clean-dirs?) 652 | (.setIgnore ignore?) 653 | (.setPaths paths) 654 | (.call)) 655 | (catch JGitInternalException e 656 | (if-not force-dirs? 657 | (throw e) 658 | (when-let [dir-path (->> (.getMessage e) 659 | (re-seq #"^Could not delete file (.*)$") 660 | (first) 661 | (last))] 662 | (if (retries dir-path) 663 | (throw e) 664 | (fs/delete-dir dir-path)) 665 | #(clean-loop (conj retries dir-path)))))))] 666 | (trampoline clean-loop #{}))) 667 | 668 | (defn blame-result 669 | [blame] 670 | (.computeAll blame) 671 | (letfn [(blame-line [num] 672 | (when-let [commit (try 673 | (.getSourceCommit blame num) 674 | (catch ArrayIndexOutOfBoundsException _ nil))] 675 | {:author (util/person-ident (.getSourceAuthor blame num)) 676 | :commit commit 677 | :committer (util/person-ident (.getSourceCommitter blame num)) 678 | :line (.getSourceLine blame num) 679 | :line-contents (-> blame .getResultContents (.getString num)) 680 | :source-path (.getSourcePath blame num)})) 681 | (blame-seq [num] 682 | (when-let [cur (blame-line num)] 683 | (cons cur 684 | (lazy-seq (blame-seq (inc num))))))] 685 | (blame-seq 0))) 686 | 687 | (defn git-blame 688 | ([^Git repo ^String path] 689 | (git-blame repo path false)) 690 | ([^Git repo ^String path ^Boolean follow-renames?] 691 | (-> repo 692 | .blame 693 | (.setFilePath path) 694 | (.setFollowFileRenames follow-renames?) 695 | .call 696 | blame-result)) 697 | ([^Git repo ^String path ^Boolean follow-renames? ^AnyObjectId start-commit] 698 | (-> repo 699 | .blame 700 | (.setFilePath path) 701 | (.setFollowFileRenames follow-renames?) 702 | (.setStartCommit start-commit) 703 | .call 704 | blame-result))) 705 | 706 | (defn get-blob-id 707 | [repo commit path] 708 | (let [tree-walk (TreeWalk/forPath (.getRepository repo) path (.getTree commit))] 709 | (when tree-walk 710 | (.getObjectId tree-walk 0)))) 711 | 712 | (defn get-blob 713 | [repo commit path] 714 | (when-let [blob-id (get-blob-id repo commit path)] 715 | (.getName blob-id))) 716 | --------------------------------------------------------------------------------