├── .gitignore ├── .travis.yml ├── LICENSE.txt ├── README.md ├── boot.properties ├── build.boot ├── src └── gitalin │ ├── adapter.clj │ ├── core.clj │ ├── git │ ├── blob.clj │ ├── coerce.clj │ ├── commit.clj │ ├── ident.clj │ ├── reference.clj │ ├── repo.clj │ ├── tag.clj │ └── tree.clj │ ├── objects.clj │ ├── protocols.clj │ ├── query.clj │ └── transit.clj └── test └── gitalin └── test ├── core.clj ├── query.clj ├── setup.clj └── transact.clj /.gitignore: -------------------------------------------------------------------------------- 1 | .nrepl-history 2 | .nrepl-port 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: clojure 2 | jdk: 3 | - oraclejdk8 4 | install: 5 | - wget -O boot https://github.com/boot-clj/boot-bin/releases/download/2.4.2/boot.sh 6 | - chmod 755 boot 7 | - ./boot -V 8 | env: 9 | - BOOT_JVM_OPTIONS="-Xmx1024m -Xms256m -Xss1m" 10 | script: ./boot test -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 2.1, February 1999 3 | 4 | Copyright (C) 1991, 1999 Free Software Foundation, Inc. 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | [This is the first released version of the Lesser GPL. It also counts 10 | as the successor of the GNU Library Public License, version 2, hence 11 | the version number 2.1.] 12 | 13 | Preamble 14 | 15 | The licenses for most software are designed to take away your 16 | freedom to share and change it. By contrast, the GNU General Public 17 | Licenses are intended to guarantee your freedom to share and change 18 | free software--to make sure the software is free for all its users. 19 | 20 | This license, the Lesser General Public License, applies to some 21 | specially designated software packages--typically libraries--of the 22 | Free Software Foundation and other authors who decide to use it. You 23 | can use it too, but we suggest you first think carefully about whether 24 | this license or the ordinary General Public License is the better 25 | strategy to use in any particular case, based on the explanations below. 26 | 27 | When we speak of free software, we are referring to freedom of use, 28 | not price. Our General Public Licenses are designed to make sure that 29 | you have the freedom to distribute copies of free software (and charge 30 | for this service if you wish); that you receive source code or can get 31 | it if you want it; that you can change the software and use pieces of 32 | it in new free programs; and that you are informed that you can do 33 | these things. 34 | 35 | To protect your rights, we need to make restrictions that forbid 36 | distributors to deny you these rights or to ask you to surrender these 37 | rights. These restrictions translate to certain responsibilities for 38 | you if you distribute copies of the library or if you modify it. 39 | 40 | For example, if you distribute copies of the library, whether gratis 41 | or for a fee, you must give the recipients all the rights that we gave 42 | you. You must make sure that they, too, receive or can get the source 43 | code. If you link other code with the library, you must provide 44 | complete object files to the recipients, so that they can relink them 45 | with the library after making changes to the library and recompiling 46 | it. And you must show them these terms so they know their rights. 47 | 48 | We protect your rights with a two-step method: (1) we copyright the 49 | library, and (2) we offer you this license, which gives you legal 50 | permission to copy, distribute and/or modify the library. 51 | 52 | To protect each distributor, we want to make it very clear that 53 | there is no warranty for the free library. Also, if the library is 54 | modified by someone else and passed on, the recipients should know 55 | that what they have is not the original version, so that the original 56 | author's reputation will not be affected by problems that might be 57 | introduced by others. 58 | 59 | Finally, software patents pose a constant threat to the existence of 60 | any free program. We wish to make sure that a company cannot 61 | effectively restrict the users of a free program by obtaining a 62 | restrictive license from a patent holder. Therefore, we insist that 63 | any patent license obtained for a version of the library must be 64 | consistent with the full freedom of use specified in this license. 65 | 66 | Most GNU software, including some libraries, is covered by the 67 | ordinary GNU General Public License. This license, the GNU Lesser 68 | General Public License, applies to certain designated libraries, and 69 | is quite different from the ordinary General Public License. We use 70 | this license for certain libraries in order to permit linking those 71 | libraries into non-free programs. 72 | 73 | When a program is linked with a library, whether statically or using 74 | a shared library, the combination of the two is legally speaking a 75 | combined work, a derivative of the original library. The ordinary 76 | General Public License therefore permits such linking only if the 77 | entire combination fits its criteria of freedom. The Lesser General 78 | Public License permits more lax criteria for linking other code with 79 | the library. 80 | 81 | We call this license the "Lesser" General Public License because it 82 | does Less to protect the user's freedom than the ordinary General 83 | Public License. It also provides other free software developers Less 84 | of an advantage over competing non-free programs. These disadvantages 85 | are the reason we use the ordinary General Public License for many 86 | libraries. However, the Lesser license provides advantages in certain 87 | special circumstances. 88 | 89 | For example, on rare occasions, there may be a special need to 90 | encourage the widest possible use of a certain library, so that it becomes 91 | a de-facto standard. To achieve this, non-free programs must be 92 | allowed to use the library. A more frequent case is that a free 93 | library does the same job as widely used non-free libraries. In this 94 | case, there is little to gain by limiting the free library to free 95 | software only, so we use the Lesser General Public License. 96 | 97 | In other cases, permission to use a particular library in non-free 98 | programs enables a greater number of people to use a large body of 99 | free software. For example, permission to use the GNU C Library in 100 | non-free programs enables many more people to use the whole GNU 101 | operating system, as well as its variant, the GNU/Linux operating 102 | system. 103 | 104 | Although the Lesser General Public License is Less protective of the 105 | users' freedom, it does ensure that the user of a program that is 106 | linked with the Library has the freedom and the wherewithal to run 107 | that program using a modified version of the Library. 108 | 109 | The precise terms and conditions for copying, distribution and 110 | modification follow. Pay close attention to the difference between a 111 | "work based on the library" and a "work that uses the library". The 112 | former contains code derived from the library, whereas the latter must 113 | be combined with the library in order to run. 114 | 115 | GNU LESSER GENERAL PUBLIC LICENSE 116 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 117 | 118 | 0. This License Agreement applies to any software library or other 119 | program which contains a notice placed by the copyright holder or 120 | other authorized party saying it may be distributed under the terms of 121 | this Lesser General Public License (also called "this License"). 122 | Each licensee is addressed as "you". 123 | 124 | A "library" means a collection of software functions and/or data 125 | prepared so as to be conveniently linked with application programs 126 | (which use some of those functions and data) to form executables. 127 | 128 | The "Library", below, refers to any such software library or work 129 | which has been distributed under these terms. A "work based on the 130 | Library" means either the Library or any derivative work under 131 | copyright law: that is to say, a work containing the Library or a 132 | portion of it, either verbatim or with modifications and/or translated 133 | straightforwardly into another language. (Hereinafter, translation is 134 | included without limitation in the term "modification".) 135 | 136 | "Source code" for a work means the preferred form of the work for 137 | making modifications to it. For a library, complete source code means 138 | all the source code for all modules it contains, plus any associated 139 | interface definition files, plus the scripts used to control compilation 140 | and installation of the library. 141 | 142 | Activities other than copying, distribution and modification are not 143 | covered by this License; they are outside its scope. The act of 144 | running a program using the Library is not restricted, and output from 145 | such a program is covered only if its contents constitute a work based 146 | on the Library (independent of the use of the Library in a tool for 147 | writing it). Whether that is true depends on what the Library does 148 | and what the program that uses the Library does. 149 | 150 | 1. You may copy and distribute verbatim copies of the Library's 151 | complete source code as you receive it, in any medium, provided that 152 | you conspicuously and appropriately publish on each copy an 153 | appropriate copyright notice and disclaimer of warranty; keep intact 154 | all the notices that refer to this License and to the absence of any 155 | warranty; and distribute a copy of this License along with the 156 | Library. 157 | 158 | You may charge a fee for the physical act of transferring a copy, 159 | and you may at your option offer warranty protection in exchange for a 160 | fee. 161 | 162 | 2. You may modify your copy or copies of the Library or any portion 163 | of it, thus forming a work based on the Library, and copy and 164 | distribute such modifications or work under the terms of Section 1 165 | above, provided that you also meet all of these conditions: 166 | 167 | a) The modified work must itself be a software library. 168 | 169 | b) You must cause the files modified to carry prominent notices 170 | stating that you changed the files and the date of any change. 171 | 172 | c) You must cause the whole of the work to be licensed at no 173 | charge to all third parties under the terms of this License. 174 | 175 | d) If a facility in the modified Library refers to a function or a 176 | table of data to be supplied by an application program that uses 177 | the facility, other than as an argument passed when the facility 178 | is invoked, then you must make a good faith effort to ensure that, 179 | in the event an application does not supply such function or 180 | table, the facility still operates, and performs whatever part of 181 | its purpose remains meaningful. 182 | 183 | (For example, a function in a library to compute square roots has 184 | a purpose that is entirely well-defined independent of the 185 | application. Therefore, Subsection 2d requires that any 186 | application-supplied function or table used by this function must 187 | be optional: if the application does not supply it, the square 188 | root function must still compute square roots.) 189 | 190 | These requirements apply to the modified work as a whole. If 191 | identifiable sections of that work are not derived from the Library, 192 | and can be reasonably considered independent and separate works in 193 | themselves, then this License, and its terms, do not apply to those 194 | sections when you distribute them as separate works. But when you 195 | distribute the same sections as part of a whole which is a work based 196 | on the Library, the distribution of the whole must be on the terms of 197 | this License, whose permissions for other licensees extend to the 198 | entire whole, and thus to each and every part regardless of who wrote 199 | it. 200 | 201 | Thus, it is not the intent of this section to claim rights or contest 202 | your rights to work written entirely by you; rather, the intent is to 203 | exercise the right to control the distribution of derivative or 204 | collective works based on the Library. 205 | 206 | In addition, mere aggregation of another work not based on the Library 207 | with the Library (or with a work based on the Library) on a volume of 208 | a storage or distribution medium does not bring the other work under 209 | the scope of this License. 210 | 211 | 3. You may opt to apply the terms of the ordinary GNU General Public 212 | License instead of this License to a given copy of the Library. To do 213 | this, you must alter all the notices that refer to this License, so 214 | that they refer to the ordinary GNU General Public License, version 2, 215 | instead of to this License. (If a newer version than version 2 of the 216 | ordinary GNU General Public License has appeared, then you can specify 217 | that version instead if you wish.) Do not make any other change in 218 | these notices. 219 | 220 | Once this change is made in a given copy, it is irreversible for 221 | that copy, so the ordinary GNU General Public License applies to all 222 | subsequent copies and derivative works made from that copy. 223 | 224 | This option is useful when you wish to copy part of the code of 225 | the Library into a program that is not a library. 226 | 227 | 4. You may copy and distribute the Library (or a portion or 228 | derivative of it, under Section 2) in object code or executable form 229 | under the terms of Sections 1 and 2 above provided that you accompany 230 | it with the complete corresponding machine-readable source code, which 231 | must be distributed under the terms of Sections 1 and 2 above on a 232 | medium customarily used for software interchange. 233 | 234 | If distribution of object code is made by offering access to copy 235 | from a designated place, then offering equivalent access to copy the 236 | source code from the same place satisfies the requirement to 237 | distribute the source code, even though third parties are not 238 | compelled to copy the source along with the object code. 239 | 240 | 5. A program that contains no derivative of any portion of the 241 | Library, but is designed to work with the Library by being compiled or 242 | linked with it, is called a "work that uses the Library". Such a 243 | work, in isolation, is not a derivative work of the Library, and 244 | therefore falls outside the scope of this License. 245 | 246 | However, linking a "work that uses the Library" with the Library 247 | creates an executable that is a derivative of the Library (because it 248 | contains portions of the Library), rather than a "work that uses the 249 | library". The executable is therefore covered by this License. 250 | Section 6 states terms for distribution of such executables. 251 | 252 | When a "work that uses the Library" uses material from a header file 253 | that is part of the Library, the object code for the work may be a 254 | derivative work of the Library even though the source code is not. 255 | Whether this is true is especially significant if the work can be 256 | linked without the Library, or if the work is itself a library. The 257 | threshold for this to be true is not precisely defined by law. 258 | 259 | If such an object file uses only numerical parameters, data 260 | structure layouts and accessors, and small macros and small inline 261 | functions (ten lines or less in length), then the use of the object 262 | file is unrestricted, regardless of whether it is legally a derivative 263 | work. (Executables containing this object code plus portions of the 264 | Library will still fall under Section 6.) 265 | 266 | Otherwise, if the work is a derivative of the Library, you may 267 | distribute the object code for the work under the terms of Section 6. 268 | Any executables containing that work also fall under Section 6, 269 | whether or not they are linked directly with the Library itself. 270 | 271 | 6. As an exception to the Sections above, you may also combine or 272 | link a "work that uses the Library" with the Library to produce a 273 | work containing portions of the Library, and distribute that work 274 | under terms of your choice, provided that the terms permit 275 | modification of the work for the customer's own use and reverse 276 | engineering for debugging such modifications. 277 | 278 | You must give prominent notice with each copy of the work that the 279 | Library is used in it and that the Library and its use are covered by 280 | this License. You must supply a copy of this License. If the work 281 | during execution displays copyright notices, you must include the 282 | copyright notice for the Library among them, as well as a reference 283 | directing the user to the copy of this License. Also, you must do one 284 | of these things: 285 | 286 | a) Accompany the work with the complete corresponding 287 | machine-readable source code for the Library including whatever 288 | changes were used in the work (which must be distributed under 289 | Sections 1 and 2 above); and, if the work is an executable linked 290 | with the Library, with the complete machine-readable "work that 291 | uses the Library", as object code and/or source code, so that the 292 | user can modify the Library and then relink to produce a modified 293 | executable containing the modified Library. (It is understood 294 | that the user who changes the contents of definitions files in the 295 | Library will not necessarily be able to recompile the application 296 | to use the modified definitions.) 297 | 298 | b) Use a suitable shared library mechanism for linking with the 299 | Library. A suitable mechanism is one that (1) uses at run time a 300 | copy of the library already present on the user's computer system, 301 | rather than copying library functions into the executable, and (2) 302 | will operate properly with a modified version of the library, if 303 | the user installs one, as long as the modified version is 304 | interface-compatible with the version that the work was made with. 305 | 306 | c) Accompany the work with a written offer, valid for at 307 | least three years, to give the same user the materials 308 | specified in Subsection 6a, above, for a charge no more 309 | than the cost of performing this distribution. 310 | 311 | d) If distribution of the work is made by offering access to copy 312 | from a designated place, offer equivalent access to copy the above 313 | specified materials from the same place. 314 | 315 | e) Verify that the user has already received a copy of these 316 | materials or that you have already sent this user a copy. 317 | 318 | For an executable, the required form of the "work that uses the 319 | Library" must include any data and utility programs needed for 320 | reproducing the executable from it. However, as a special exception, 321 | the materials to be distributed need not include anything that is 322 | normally distributed (in either source or binary form) with the major 323 | components (compiler, kernel, and so on) of the operating system on 324 | which the executable runs, unless that component itself accompanies 325 | the executable. 326 | 327 | It may happen that this requirement contradicts the license 328 | restrictions of other proprietary libraries that do not normally 329 | accompany the operating system. Such a contradiction means you cannot 330 | use both them and the Library together in an executable that you 331 | distribute. 332 | 333 | 7. You may place library facilities that are a work based on the 334 | Library side-by-side in a single library together with other library 335 | facilities not covered by this License, and distribute such a combined 336 | library, provided that the separate distribution of the work based on 337 | the Library and of the other library facilities is otherwise 338 | permitted, and provided that you do these two things: 339 | 340 | a) Accompany the combined library with a copy of the same work 341 | based on the Library, uncombined with any other library 342 | facilities. This must be distributed under the terms of the 343 | Sections above. 344 | 345 | b) Give prominent notice with the combined library of the fact 346 | that part of it is a work based on the Library, and explaining 347 | where to find the accompanying uncombined form of the same work. 348 | 349 | 8. You may not copy, modify, sublicense, link with, or distribute 350 | the Library except as expressly provided under this License. Any 351 | attempt otherwise to copy, modify, sublicense, link with, or 352 | distribute the Library is void, and will automatically terminate your 353 | rights under this License. However, parties who have received copies, 354 | or rights, from you under this License will not have their licenses 355 | terminated so long as such parties remain in full compliance. 356 | 357 | 9. You are not required to accept this License, since you have not 358 | signed it. However, nothing else grants you permission to modify or 359 | distribute the Library or its derivative works. These actions are 360 | prohibited by law if you do not accept this License. Therefore, by 361 | modifying or distributing the Library (or any work based on the 362 | Library), you indicate your acceptance of this License to do so, and 363 | all its terms and conditions for copying, distributing or modifying 364 | the Library or works based on it. 365 | 366 | 10. Each time you redistribute the Library (or any work based on the 367 | Library), the recipient automatically receives a license from the 368 | original licensor to copy, distribute, link with or modify the Library 369 | subject to these terms and conditions. You may not impose any further 370 | restrictions on the recipients' exercise of the rights granted herein. 371 | You are not responsible for enforcing compliance by third parties with 372 | this License. 373 | 374 | 11. If, as a consequence of a court judgment or allegation of patent 375 | infringement or for any other reason (not limited to patent issues), 376 | conditions are imposed on you (whether by court order, agreement or 377 | otherwise) that contradict the conditions of this License, they do not 378 | excuse you from the conditions of this License. If you cannot 379 | distribute so as to satisfy simultaneously your obligations under this 380 | License and any other pertinent obligations, then as a consequence you 381 | may not distribute the Library at all. For example, if a patent 382 | license would not permit royalty-free redistribution of the Library by 383 | all those who receive copies directly or indirectly through you, then 384 | the only way you could satisfy both it and this License would be to 385 | refrain entirely from distribution of the Library. 386 | 387 | If any portion of this section is held invalid or unenforceable under any 388 | particular circumstance, the balance of the section is intended to apply, 389 | and the section as a whole is intended to apply in other circumstances. 390 | 391 | It is not the purpose of this section to induce you to infringe any 392 | patents or other property right claims or to contest validity of any 393 | such claims; this section has the sole purpose of protecting the 394 | integrity of the free software distribution system which is 395 | implemented by public license practices. Many people have made 396 | generous contributions to the wide range of software distributed 397 | through that system in reliance on consistent application of that 398 | system; it is up to the author/donor to decide if he or she is willing 399 | to distribute software through any other system and a licensee cannot 400 | impose that choice. 401 | 402 | This section is intended to make thoroughly clear what is believed to 403 | be a consequence of the rest of this License. 404 | 405 | 12. If the distribution and/or use of the Library is restricted in 406 | certain countries either by patents or by copyrighted interfaces, the 407 | original copyright holder who places the Library under this License may add 408 | an explicit geographical distribution limitation excluding those countries, 409 | so that distribution is permitted only in or among countries not thus 410 | excluded. In such case, this License incorporates the limitation as if 411 | written in the body of this License. 412 | 413 | 13. The Free Software Foundation may publish revised and/or new 414 | versions of the Lesser General Public License from time to time. 415 | Such new versions will be similar in spirit to the present version, 416 | but may differ in detail to address new problems or concerns. 417 | 418 | Each version is given a distinguishing version number. If the Library 419 | specifies a version number of this License which applies to it and 420 | "any later version", you have the option of following the terms and 421 | conditions either of that version or of any later version published by 422 | the Free Software Foundation. If the Library does not specify a 423 | license version number, you may choose any version ever published by 424 | the Free Software Foundation. 425 | 426 | 14. If you wish to incorporate parts of the Library into other free 427 | programs whose distribution conditions are incompatible with these, 428 | write to the author to ask for permission. For software which is 429 | copyrighted by the Free Software Foundation, write to the Free 430 | Software Foundation; we sometimes make exceptions for this. Our 431 | decision will be guided by the two goals of preserving the free status 432 | of all derivatives of our free software and of promoting the sharing 433 | and reuse of software generally. 434 | 435 | NO WARRANTY 436 | 437 | 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO 438 | WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. 439 | EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR 440 | OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY 441 | KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE 442 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 443 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE 444 | LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME 445 | THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 446 | 447 | 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN 448 | WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY 449 | AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU 450 | FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR 451 | CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE 452 | LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING 453 | RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A 454 | FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF 455 | SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH 456 | DAMAGES. 457 | 458 | END OF TERMS AND CONDITIONS 459 | 460 | How to Apply These Terms to Your New Libraries 461 | 462 | If you develop a new library, and you want it to be of the greatest 463 | possible use to the public, we recommend making it free software that 464 | everyone can redistribute and change. You can do so by permitting 465 | redistribution under these terms (or, alternatively, under the terms of the 466 | ordinary General Public License). 467 | 468 | To apply these terms, attach the following notices to the library. It is 469 | safest to attach them to the start of each source file to most effectively 470 | convey the exclusion of warranty; and each file should have at least the 471 | "copyright" line and a pointer to where the full notice is found. 472 | 473 | 474 | Copyright (C) 475 | 476 | This library is free software; you can redistribute it and/or 477 | modify it under the terms of the GNU Lesser General Public 478 | License as published by the Free Software Foundation; either 479 | version 2.1 of the License, or (at your option) any later version. 480 | 481 | This library is distributed in the hope that it will be useful, 482 | but WITHOUT ANY WARRANTY; without even the implied warranty of 483 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 484 | Lesser General Public License for more details. 485 | 486 | You should have received a copy of the GNU Lesser General Public 487 | License along with this library; if not, write to the Free Software 488 | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 489 | 490 | Also add information on how to contact you by electronic and paper mail. 491 | 492 | You should also get your employer (if you work as a programmer) or your 493 | school, if any, to sign a "copyright disclaimer" for the library, if 494 | necessary. Here is a sample; alter the names: 495 | 496 | Yoyodyne, Inc., hereby disclaims all copyright interest in the 497 | library `Frob' (a library for tweaking knobs) written by James Random Hacker. 498 | 499 | , 1 April 1990 500 | Ty Coon, President of Vice 501 | 502 | That's all there is to it! 503 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Gitalin 2 | 3 | > *"Experts call for caution over Gitalin"* 4 | > — BBC News (not really) 5 | 6 | gitalin is an object store for Clojure based on Git. Its data model 7 | is simple: there are 8 | 9 | * references (branches and tags), 10 | * commits (= transactions), 11 | * objects (entities with (almost) arbitrary properties). 12 | 13 | All of the above can be queried using a language similar to Datalog 14 | and modified using a transaction interface. 15 | 16 | Other notable characteristics include: 17 | 18 | 1. Easy setup: all it needs is an empty Git repository. 19 | 2. Easy export/backup/restore by cloning and pushing elsewhere. 20 | 3. Possibility to inspect data using Git's command-line interface. 21 | 22 | **Note: This is work in progress. Not everything described here will work.** 23 | 24 | ## Status 25 | 26 | [![Build Status](https://travis-ci.org/Jannis/gitalin.svg?branch=master)](https://travis-ci.org/Jannis/gitalin) 27 | 28 | * Done: Basic query interface 29 | * Done: Basic transaction interface 30 | * TODO: Update/remove mutations. 31 | * TODO: Temporary IDs. 32 | * TODO: Direct entity access - what format to return? 33 | * TODO: Allow multiple connections to be used in a single query and 34 | * TODO: Functions, not just pattern clauses. 35 | * TODO: Tagging. 36 | * TODO: Validation and error handling. 37 | allow clauses with connections like `[[?conn ?ref :ref/name ...]]` 38 | in combination with `:in [?conn ?other-conn]`. This would allow to 39 | query across multiple repositories. 40 | 41 | ### Future ideas 42 | 43 | * Pull API 44 | * Signed transactions 45 | 46 | ## Quick start 47 | 48 | Clone Gitalin: 49 | 50 | ``` 51 | git clone https://github.com/jannis/gitalin 52 | cd gitalin 53 | ``` 54 | 55 | Start a REPL: 56 | 57 | ``` 58 | boot repl 59 | ``` 60 | 61 | Create a store and connect to it: 62 | 63 | ``` 64 | (use '[gitalin.core :as g]) 65 | 66 | (g/create-store! "/tmp/test-store") 67 | 68 | (def store (g/default-adapter "/tmp/test-store")) 69 | (def conn (g/connect store)) 70 | ``` 71 | 72 | Now you are ready to create objects using transactions: 73 | 74 | ``` 75 | (def id (g/tempid)) 76 | 77 | (let [id (g/tempid)] 78 | (g/transact! conn 79 | {:target "HEAD" 80 | :author {:name "Your Name" :email "your@email.org"} 81 | :committer {:name "Your Name" :email "your@email.org"} 82 | :message "Create John"} 83 | [[:object/add id :person/name "John"] 84 | [:object/set id :person/email "john@email.org"]])) 85 | ``` 86 | 87 | You can also query references, commits and objects using the query 88 | interface: 89 | 90 | ``` 91 | (g/q conn '{:find [?n ?e] 92 | :where [[?ref :ref/name "HEAD"] 93 | [?ref :ref/commit ?commit] 94 | [?commit :commit/object ?o] 95 | [?o :person/name ?n] 96 | [?o :person/email ?e]]}) 97 | 98 | -> #{["John" "john@email.org"]} 99 | ``` 100 | 101 | You already know which commit `HEAD` is on and you only want 102 | Clarice's email address? Even better: 103 | 104 | ``` 105 | (g/q conn 106 | '{:find ?e 107 | :in [?commit ?name] 108 | :where [[?commit :commit/object ?o] 109 | [?o :person/name ?name] 110 | [?o :person/email ?e]]} 111 | "commit/09a9202377d81198d409391ca54376d9c3eaadf2" 112 | "Clarice") 113 | 114 | -> #{"clarice@her-domain.com"} 115 | ``` 116 | 117 | You want to know the IDs of all objects in the second most recent 118 | transaction in `HEAD`? 119 | 120 | ``` 121 | (g/q conn '{:find ?o 122 | :where [[?ref :ref/name "HEAD"] 123 | [?ref :ref/commit ?commit] 124 | [?commit :commit/parent ?parent] 125 | [?parent :commit/object ?o]]}) 126 | 127 | -> #{"object/09a9202377d81198d409391ca54376d9c3eaadf2/5de37b78-bcb7-482f-bff9-d8dd113a8583" 128 | "object/09a9202377d81198d409391ca54376d9c3eaadf2/d91b867e-1ba0-449b-a106-3489927b6803"} 129 | ``` 130 | 131 | You can also query for all objects that ever existed in the store: 132 | 133 | ``` 134 | (g/q conn '{:find ?o 135 | :where [[?o :object/id ?u]]}) 136 | 137 | -> #{"object/09a9202377d81198d409391ca54376d9c3eaadf2/5de37b78-bcb7-482f-bff9-d8dd113a8583" 138 | "object/09a9202377d81198d409391ca54376d9c3eaadf2/d91b867e-1ba0-449b-a106-3489927b6803" 139 | "object/c0cb7699274b80eb585062666a2922f5d4082913/e2b3e246-ac12-4d7a-82a1-349d4e8fdf89"} 140 | ``` 141 | 142 | There are various ways on how to improve and shorten queries. 143 | 144 | ## Query interface 145 | 146 | TODO 147 | 148 | ## Direct entity access 149 | 150 | TODO 151 | 152 | ## Transaction interface 153 | 154 | TODO 155 | 156 | ## License 157 | 158 | Copyright (c) 2015 Jannis Pohlmann 159 | 160 | Licensed under [GNU LGPL v2.1](http://www.gnu.org/licenses/old-licenses/lgpl-2.1.en.html). 161 | -------------------------------------------------------------------------------- /boot.properties: -------------------------------------------------------------------------------- 1 | BOOT_CLOJURE_VERSION=1.7.0 2 | BOOT_VERSION=2.4.2 3 | -------------------------------------------------------------------------------- /build.boot: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env boot 2 | 3 | (set-env! 4 | :source-paths #{"test"} 5 | :resource-paths #{"src"} 6 | :dependencies '[;; Boot 7 | [adzerk/boot-test "1.0.5" :scope "test"] 8 | 9 | ;; Testing 10 | [org.clojure/test.check "0.9.0" :scope "test"] 11 | [me.raynes/fs "1.4.6" :scope "test"] 12 | 13 | ;; General 14 | [clj-jgit "0.8.8"] 15 | [com.cognitect/transit-clj "0.8.281"]]) 16 | 17 | (task-options! 18 | pom {:project 'gitalin 19 | :version "0.1.0-SNAPSHOT"}) 20 | 21 | (require '[adzerk.boot-test :refer :all]) 22 | -------------------------------------------------------------------------------- /src/gitalin/adapter.clj: -------------------------------------------------------------------------------- 1 | (ns gitalin.adapter 2 | (:require [clojure.string :as str] 3 | [gitalin.git.coerce :refer [to-oid]] 4 | [gitalin.git.commit :as commit] 5 | [gitalin.git.ident :as ident] 6 | [gitalin.git.repo :as git-repo] 7 | [gitalin.git.reference :as reference] 8 | [gitalin.git.tree :as tree] 9 | [gitalin.objects :as objects] 10 | [gitalin.protocols :as p]) 11 | (:import (gitalin.git.commit Commit) 12 | (gitalin.git.reference Reference) 13 | (gitalin.objects GitalinObject))) 14 | 15 | ;;;; Temporary IDs 16 | 17 | (defrecord TempId [uuid]) 18 | 19 | (defn tempid [] 20 | (TempId. (str (java.util.UUID/randomUUID)))) 21 | 22 | (defn tempid? [id] 23 | (instance? TempId id)) 24 | 25 | ;;;; Transaction context 26 | 27 | (defrecord TransactionContext [repo tree tempids]) 28 | 29 | ;;;; Transactions 30 | 31 | (defmulti mutate-step (fn [_ mutation] (first mutation))) 32 | 33 | (defmethod mutate-step :object/add 34 | [context [_ uuid property value]] 35 | (let [repo (:repo context) 36 | tree (:tree context) 37 | real-uuid (if (tempid? uuid) 38 | (:uuid uuid) 39 | uuid) 40 | new-tempids (if (tempid? uuid) 41 | (assoc (:tempids context) uuid real-uuid) 42 | (:tempids context)) 43 | object (objects/make real-uuid {property value}) 44 | blob (objects/make-blob repo object) 45 | entry (objects/to-tree-entry object blob) 46 | new-tree (tree/update-entry repo (:tree context) entry)] 47 | (-> context 48 | (assoc :tree new-tree) 49 | (assoc :tempids new-tempids)))) 50 | 51 | (defmethod mutate-step :object/set 52 | [context [_ uuid property value]] 53 | (let [repo (:repo context) 54 | tree (:tree context) 55 | real-uuid (if (tempid? uuid) 56 | (get-in context [:tempids uuid]) 57 | uuid) 58 | object (objects/load repo tree real-uuid) 59 | object (assoc-in object [:properties property] value) 60 | blob (objects/make-blob repo object) 61 | entry (objects/to-tree-entry object blob) 62 | new-tree (tree/update-entry repo tree entry)] 63 | (-> context 64 | (assoc :tree new-tree)))) 65 | 66 | (defmethod mutate-step :object/unset 67 | [context [_ uuid property value]] 68 | (let [repo (:repo context) 69 | tree (:tree context) 70 | real-uuid (if (tempid? uuid) 71 | (get-in context [:tempids uuid]) 72 | uuid) 73 | object (objects/load repo tree real-uuid) 74 | object (update-in object :properties dissoc property) 75 | blob (objects/make-blob repo object) 76 | entry (objects/to-tree-entry object blob) 77 | new-tree (tree/update-entry repo tree entry)] 78 | (-> context 79 | (assoc :tree new-tree)))) 80 | 81 | (defn commit! [repo info parents tree] 82 | (commit/make repo tree parents 83 | :author (ident/from-map (:author info)) 84 | :committer (ident/from-map (or (:committer info) 85 | (:author info))) 86 | :message (:message info))) 87 | 88 | (defn update-reference! [repo info commit] 89 | (when-let [target (reference/load repo (:target info))] 90 | (when (reference/update! repo target commit) 91 | (reference/load repo (:target info))))) 92 | 93 | ;;;; Conversion between objects and IDs 94 | 95 | (defn obj->id [obj] 96 | (cond 97 | (instance? Reference obj) 98 | (str "reference/" (:name obj)) 99 | 100 | (instance? Commit obj) 101 | (str "commit/" (:sha1 obj)) 102 | 103 | (instance? GitalinObject obj) 104 | (str "object/" (-> obj meta :commit :sha1) 105 | "/" (:uuid obj)) 106 | 107 | :else 108 | nil)) 109 | 110 | (defn id->obj [repo id] 111 | (let [segments (str/split id #"/")] 112 | (case (first segments) 113 | "reference" 114 | (let [[_ name] segments] 115 | (reference/load repo name)) 116 | 117 | "commit" 118 | (let [[_ sha1] segments] 119 | (commit/load repo (to-oid repo sha1))) 120 | 121 | "object" 122 | (let [[_ sha1 uuid] segments 123 | commit' (commit/load repo (to-oid repo sha1)) 124 | tree (commit/tree repo commit') 125 | object (objects/load repo tree uuid)] 126 | (vary-meta object assoc :commit commit')) 127 | 128 | :else 129 | nil))) 130 | 131 | ;;;; Entities 132 | 133 | (defrecord Entity [id properties] 134 | p/IEntity 135 | (id [this] 136 | id) 137 | (properties [this] 138 | properties)) 139 | 140 | (defn finalize-props [props] 141 | (into [] (filter #(not (nil? (second %))) props))) 142 | 143 | (defn reference->entity [repo ref] 144 | (let [props [[:ref/name (:name ref)] 145 | [:ref/commit (some->> ref :head obj->id)] 146 | [:ref/type (:type ref)]]] 147 | (Entity. (obj->id ref) (finalize-props props)))) 148 | 149 | (defn commit->entity [repo com] 150 | (let [tree (commit/tree repo com) 151 | parents (some->> (:parents com) 152 | (map #(to-oid repo %)) 153 | (map #(commit/load repo %)) 154 | (mapv obj->id)) 155 | objects (some->> (objects/load-all repo tree) 156 | (map #(vary-meta % assoc :commit com)) 157 | (map obj->id)) 158 | props [[:commit/sha1 (:sha1 com)] 159 | [:commit/author (:author com)] 160 | [:commit/committer (:committer com)] 161 | [:commit/message (:message com)]]] 162 | (Entity. (obj->id com) 163 | (finalize-props 164 | (concat props 165 | (mapv #(vector :commit/parent %) parents) 166 | (mapv #(vector :commit/object %) objects)))))) 167 | 168 | (defn object->entity [repo object] 169 | (let [com (:commit (meta object)) 170 | props [[:object/uuid (:uuid object)] 171 | [:object/commit (obj->id com)]]] 172 | (Entity. (obj->id object) 173 | (finalize-props 174 | (concat props 175 | (mapv (fn [[prop val]] [prop val]) 176 | (:properties object))))))) 177 | 178 | ;;;; Loading objects from Git 179 | 180 | (defn collect-commits [repo commit] 181 | (flatten 182 | (into [commit] 183 | (mapv (fn [sha1] 184 | (->> sha1 185 | (to-oid repo) 186 | (commit/load repo) 187 | (collect-commits repo))) 188 | (:parents commit))))) 189 | 190 | (defn collect-objects [repo commit'] 191 | (let [tree (commit/tree repo commit')] 192 | (map (fn [object] 193 | (-> object 194 | (vary-meta assoc :commit commit'))) 195 | (objects/load-all repo tree)))) 196 | 197 | (defn load-all-references [repo] 198 | (reference/load-all repo)) 199 | 200 | (defn load-all-commits [repo] 201 | (->> (load-all-references repo) 202 | (map :head) 203 | (map #(collect-commits repo %)) 204 | (apply concat) 205 | (set))) 206 | 207 | (defn load-all-objects [repo] 208 | (->> (load-all-commits repo) 209 | (map #(collect-objects repo %)) 210 | (apply concat) 211 | (set))) 212 | 213 | ;;;; Default adapter implementation 214 | 215 | (defrecord Adapter [path repo] 216 | p/IAdapter 217 | (connect [this] 218 | (assoc this :repo (git-repo/load path))) 219 | 220 | (disconnect [this] 221 | (dissoc this :repo)) 222 | 223 | (references [this] 224 | (->> (load-all-references repo) 225 | (map #(reference->entity repo %)) 226 | (set))) 227 | 228 | (reference [this id] 229 | (->> (id->obj repo id) 230 | (reference->entity repo))) 231 | 232 | (commit [this id] 233 | (->> (id->obj repo id) 234 | (commit->entity repo))) 235 | 236 | (commits [this] 237 | (->> (load-all-commits repo) 238 | (map #(commit->entity repo %)) 239 | (set))) 240 | 241 | (objects [this] 242 | (->> (load-all-objects repo) 243 | (map #(object->entity repo %)) 244 | (set))) 245 | 246 | (object [this id] 247 | (->> (id->obj repo id) 248 | (object->entity repo))) 249 | 250 | (transact! [this info mutations] 251 | (let [base (if (:base info) 252 | (commit/load repo (to-oid repo (:base info))) 253 | (when (:target info) 254 | (some->> (reference/load repo (:target info)) 255 | :head :sha1 (to-oid repo) 256 | (commit/load repo)))) 257 | base-tree (if base 258 | (commit/tree repo base) 259 | (tree/make-empty repo)) 260 | result (reduce mutate-step 261 | (TransactionContext. repo base-tree {}) 262 | mutations)] 263 | (->> (:tree result) 264 | (commit! repo info (if base [base] [])) 265 | (update-reference! repo info))))) 266 | 267 | (defn adapter [path] 268 | (Adapter. path nil)) 269 | -------------------------------------------------------------------------------- /src/gitalin/core.clj: -------------------------------------------------------------------------------- 1 | (ns gitalin.core 2 | (:require [gitalin.git.reference :as reference] 3 | [gitalin.git.repo :as git-repo] 4 | [gitalin.adapter :as a] 5 | [gitalin.protocols :as p] 6 | [gitalin.query :as query])) 7 | 8 | ;;;; Connections 9 | 10 | (defrecord Connection [id adapter] 11 | p/IConnection 12 | (conn-id [this] 13 | id) 14 | (adapter [this] 15 | (:adapter this))) 16 | 17 | (defn adapter [conn] 18 | (p/adapter conn)) 19 | 20 | (defn connect [adapter] 21 | (Connection. (java.util.UUID/randomUUID) (p/connect adapter))) 22 | 23 | (defn connection? [conn] 24 | (instance? Connection conn)) 25 | 26 | (defn default-adapter [path] 27 | (a/adapter path)) 28 | 29 | ;;;; Store creation 30 | 31 | (defn create-store! [path] 32 | (if (git-repo/init path) path nil)) 33 | 34 | ;;;; Queries 35 | 36 | (defn q [conn q & args] 37 | {:pre [(satisfies? p/IConnection conn) 38 | (map? q)]} 39 | (query/q conn q args)) 40 | 41 | ;;;; Transactions 42 | 43 | (defn transact! [conn info mutations] 44 | {:pre [(satisfies? p/IConnection conn) 45 | (map? info) 46 | (vector? mutations)]} 47 | (p/transact! (p/adapter conn) info mutations)) 48 | -------------------------------------------------------------------------------- /src/gitalin/git/blob.clj: -------------------------------------------------------------------------------- 1 | (ns gitalin.git.blob 2 | (:import [org.eclipse.jgit.lib Constants]) 3 | (:refer-clojure :exclude [load]) 4 | (:require [gitalin.git.coerce :refer [to-sha1]] 5 | [gitalin.git.repo :refer [object-inserter object-loader]])) 6 | 7 | (defrecord Blob [sha1 data]) 8 | 9 | (defn load [repo oid] 10 | (some->> (object-loader repo oid) 11 | (.getBytes) 12 | (->Blob (to-sha1 oid)))) 13 | 14 | (defn write [repo data] 15 | (let [inserter (object-inserter repo) 16 | oid (.insert inserter (Constants/OBJ_BLOB) data)] 17 | (.flush inserter) 18 | (->Blob (to-sha1 oid) data))) 19 | -------------------------------------------------------------------------------- /src/gitalin/git/coerce.clj: -------------------------------------------------------------------------------- 1 | (ns gitalin.git.coerce 2 | (:import [org.eclipse.jgit.lib FileMode]) 3 | (:require [clojure.string :as str])) 4 | 5 | (defn to-sha1 [oid] 6 | (.getName oid)) 7 | 8 | (defn to-oid [repo sha1] 9 | (.resolve (.getRepository repo) sha1)) 10 | 11 | (defn to-git-ref-name [name] 12 | (str/replace name #":" "/")) 13 | 14 | (defn to-ref-name [name] 15 | (str/replace name #"/" ":")) 16 | 17 | (defn to-file-mode [mode] 18 | (case mode 19 | :tree FileMode/TREE 20 | :file FileMode/REGULAR_FILE 21 | nil)) 22 | -------------------------------------------------------------------------------- /src/gitalin/git/commit.clj: -------------------------------------------------------------------------------- 1 | (ns gitalin.git.commit 2 | (:import [org.eclipse.jgit.lib CommitBuilder]) 3 | (:refer-clojure :exclude [load]) 4 | (:require [gitalin.git.coerce :refer [to-oid to-sha1]] 5 | [gitalin.git.ident :as ident] 6 | [gitalin.git.repo :refer [object-inserter rev-walk]] 7 | [gitalin.git.tree :as git-tree])) 8 | 9 | (defn commit-builder [] 10 | (CommitBuilder.)) 11 | 12 | (defrecord Commit [sha1 13 | author 14 | committer 15 | subject 16 | message 17 | parents]) 18 | 19 | (defn to-commit [repo jcommit] 20 | (let [sha1 (.getName jcommit) 21 | author (ident/load (.getAuthorIdent jcommit)) 22 | committer (ident/load (.getCommitterIdent jcommit)) 23 | subject (.getShortMessage jcommit) 24 | message (.getFullMessage jcommit) 25 | parents (mapv #(to-sha1 (.getId %)) (.getParents jcommit))] 26 | (->Commit sha1 author committer subject message parents))) 27 | 28 | (defn load [repo oid] 29 | (some->> (.parseCommit (rev-walk repo) oid) 30 | (to-commit repo))) 31 | 32 | (defn to-jcommit [repo commit] 33 | (some->> (:sha1 commit) 34 | (to-oid repo) 35 | (.parseCommit (rev-walk repo)))) 36 | 37 | (defn tree [repo commit] 38 | (some->> (to-jcommit repo commit) 39 | (.getTree) 40 | (.getId) 41 | (git-tree/load repo))) 42 | 43 | (defn make [repo tree parents & {:keys [author committer message]}] 44 | (let [builder (commit-builder)] 45 | (.setTreeId builder (to-oid repo (:sha1 tree))) 46 | (doseq [parent parents] 47 | (.addParentId builder (to-oid repo (:sha1 parent)))) 48 | (.setAuthor builder (ident/to-jident author)) 49 | (.setCommitter builder (ident/to-jident committer)) 50 | (.setMessage builder message) 51 | (let [inserter (object-inserter repo) 52 | oid (.insert inserter builder)] 53 | (.flush inserter) 54 | (load repo oid)))) 55 | -------------------------------------------------------------------------------- /src/gitalin/git/ident.clj: -------------------------------------------------------------------------------- 1 | (ns gitalin.git.ident 2 | (:import [java.util Calendar Date] 3 | [org.eclipse.jgit.lib PersonIdent]) 4 | (:refer-clojure :exclude [load])) 5 | 6 | (defrecord Identity [name email date utc-offset]) 7 | 8 | (defn to-jident [ident] 9 | (PersonIdent. (:name ident) 10 | (:email ident) 11 | (.getTime (:date ident)) 12 | (:utc-offset ident))) 13 | 14 | (defn load [ident] 15 | (->Identity (.getName ident) 16 | (.getEmailAddress ident) 17 | (.getWhen ident) 18 | (.getTimeZoneOffset ident))) 19 | 20 | (defn from-map [m] 21 | (->Identity (:name m) 22 | (:email m) 23 | (Date.) 24 | (.. (Calendar/getInstance) (get Calendar/ZONE_OFFSET)))) 25 | -------------------------------------------------------------------------------- /src/gitalin/git/reference.clj: -------------------------------------------------------------------------------- 1 | (ns gitalin.git.reference 2 | (:import [org.eclipse.jgit.lib Constants RefUpdate RefUpdate$Result]) 3 | (:refer-clojure :exclude [load]) 4 | (:require [clojure.string :as str] 5 | [gitalin.git.coerce :refer [to-git-ref-name 6 | to-ref-name 7 | to-oid]] 8 | [gitalin.git.commit :as commit] 9 | [gitalin.git.repo :refer [object-type rev-walk]] 10 | [gitalin.git.tag :as tag])) 11 | 12 | (defrecord Reference [name type tag head]) 13 | 14 | (defn to-reference [repo jref] 15 | (when jref 16 | (let [git-name (.getName jref) 17 | name (to-ref-name git-name) 18 | oid (.getObjectId jref) 19 | tag? (re-matches #"^refs/tags/" git-name) 20 | annotated? (when oid (= Constants/OBJ_TAG (object-type repo oid))) 21 | tag (when annotated? (tag/load repo (.getObjectId jref))) 22 | head-oid (if tag 23 | (->> (to-oid repo (:sha1 tag)) 24 | (.parseTag (rev-walk repo)) 25 | (.getObject) 26 | (.getId)) 27 | (.getObjectId (.getLeaf jref))) 28 | head (when head-oid (commit/load repo head-oid))] 29 | (->Reference name (if tag? "tag" "branch") tag head)))) 30 | 31 | (defn load [repo name] 32 | (some->> (to-git-ref-name name) 33 | (.getRef (.getRepository repo)) 34 | (to-reference repo))) 35 | 36 | (defn load-all [repo] 37 | (into [] 38 | (map (fn [[_ ref]] (to-reference repo ref))) 39 | (.getAllRefs (.getRepository repo)))) 40 | 41 | (defn update! [repo ref commit] 42 | (let [update (.updateRef (.getRepository repo) (:name ref))] 43 | (.setNewObjectId update (to-oid repo (:sha1 commit))) 44 | (let [result (.update update)] 45 | (condp = result 46 | RefUpdate$Result/FAST_FORWARD true 47 | RefUpdate$Result/FORCED true 48 | RefUpdate$Result/NEW true 49 | RefUpdate$Result/NO_CHANGE true 50 | RefUpdate$Result/RENAMED true 51 | RefUpdate$Result/IO_FAILURE false 52 | RefUpdate$Result/LOCK_FAILURE false 53 | RefUpdate$Result/REJECTED false 54 | RefUpdate$Result/REJECTED_CURRENT_BRANCH false 55 | :else false)))) 56 | -------------------------------------------------------------------------------- /src/gitalin/git/repo.clj: -------------------------------------------------------------------------------- 1 | (ns gitalin.git.repo 2 | (:import [org.eclipse.jgit.api InitCommand] 3 | [org.eclipse.jgit.revwalk RevWalk] 4 | [org.eclipse.jgit.treewalk TreeWalk]) 5 | (:refer-clojure :exclude [load]) 6 | (:require [clojure.java.io :as io] 7 | [clj-jgit.porcelain :refer [load-repo]])) 8 | 9 | ;;;; Loading repositories 10 | 11 | (defn load [location] 12 | (load-repo location)) 13 | 14 | (defn init [location] 15 | (-> (InitCommand.) 16 | (.setDirectory (io/as-file location)) 17 | (.call)) 18 | (load location)) 19 | 20 | ;;;; Utilities for walking revisions and trees 21 | 22 | (defn rev-walk [repo] 23 | (RevWalk. (.getRepository repo))) 24 | 25 | (defn tree-walk 26 | [repo trees] 27 | (let [tw (TreeWalk. (.getRepository repo))] 28 | (doseq [t trees] (.addTree tw t)) 29 | tw)) 30 | 31 | (defn tree-walk-for-entry [repo tree path] 32 | (TreeWalk/forPath (.getRepository repo) path tree)) 33 | 34 | ;;;; Utilities for loading and creating objects 35 | 36 | (defn object-loader [repo oid] 37 | (.open (.getRepository repo) oid)) 38 | 39 | (defn object-inserter [repo] 40 | (.newObjectInserter (.getRepository repo))) 41 | 42 | (defn object-type [repo oid] 43 | (.getType (object-loader repo oid))) 44 | -------------------------------------------------------------------------------- /src/gitalin/git/tag.clj: -------------------------------------------------------------------------------- 1 | (ns gitalin.git.tag 2 | (:refer-clojure :exclude [load]) 3 | (:require [gitalin.git.ident :as ident] 4 | [gitalin.git.repo :refer [rev-walk]])) 5 | 6 | (defrecord Tag [sha1 tagger subject message]) 7 | 8 | (defn to-tag [repo jtag] 9 | (let [sha1 (.getName (.getId jtag)) 10 | tagger (ident/load (.getTaggerIdent jtag)) 11 | subject (.getShortMessage jtag) 12 | message (.getFullMessage jtag)] 13 | (->Tag sha1 tagger subject message))) 14 | 15 | (defn load [repo oid] 16 | (some->> (.parseTag (rev-walk repo) oid) 17 | (to-tag repo))) 18 | -------------------------------------------------------------------------------- /src/gitalin/git/tree.clj: -------------------------------------------------------------------------------- 1 | (ns gitalin.git.tree 2 | (:import [org.eclipse.jgit.lib FileMode TreeFormatter]) 3 | (:refer-clojure :exclude [load]) 4 | (:require [gitalin.git.coerce :refer [to-file-mode to-oid to-sha1]] 5 | [gitalin.git.repo :refer [object-inserter 6 | rev-walk 7 | tree-walk 8 | tree-walk-for-entry]])) 9 | 10 | (defn tree-formatter [] 11 | (TreeFormatter.)) 12 | 13 | (defrecord TreeEntry [name sha1 type]) 14 | (defrecord Tree [sha1 entries]) 15 | 16 | (defn entries [repo jtree] 17 | (let [walk (tree-walk repo [jtree]) 18 | entries (transient [])] 19 | (while (.next walk) 20 | (conj! entries (->TreeEntry (.getPathString walk) 21 | (.getName (.getObjectId walk 0)) 22 | (if (.isSubtree walk) :tree :file)))) 23 | (persistent! entries))) 24 | 25 | (defn to-tree [repo jtree] 26 | (let [sha1 (.getName jtree) 27 | entries (entries repo jtree)] 28 | (->Tree sha1 entries))) 29 | 30 | (defn to-jtree [repo tree] 31 | (some->> (:sha1 tree) 32 | (to-oid repo) 33 | (.parseTree (rev-walk repo)))) 34 | 35 | (defn load [repo oid] 36 | (some->> (.parseTree (rev-walk repo) oid) 37 | (to-tree repo))) 38 | 39 | (defn make-empty [repo] 40 | (let [formatter (tree-formatter) 41 | inserter (object-inserter repo) 42 | oid (.insert inserter formatter)] 43 | (do 44 | (.flush inserter) 45 | (load repo oid)))) 46 | 47 | (defn get-tree [repo tree subtree-name] 48 | (let [jtree (to-jtree repo tree) 49 | walk (tree-walk-for-entry repo jtree subtree-name)] 50 | (when walk 51 | (load repo (.getObjectId walk 0))))) 52 | 53 | (defn contains-entry? [tree name] 54 | ((complement empty?) (filter #{name} (map :name (:entries tree))))) 55 | 56 | (defn write [repo tree] 57 | (let [formatter (tree-formatter)] 58 | (doseq [entry (:entries tree)] 59 | (.append formatter (:name entry) 60 | (to-file-mode (:type entry)) 61 | (to-oid repo (:sha1 entry)))) 62 | (let [inserter (object-inserter repo) 63 | oid (.insert inserter formatter)] 64 | (.flush inserter) 65 | oid))) 66 | 67 | (defn update-entry [repo tree entry] 68 | (->> (update tree :entries (fn [entries] 69 | (conj (remove #(= (:name %) (:name entry)) 70 | entries) 71 | entry))) 72 | (write repo) 73 | (load repo))) 74 | 75 | (defn remove-entry [repo tree entry] 76 | (->> (update tree :entries (fn [entries] 77 | (remove #(= (:name %) (:name entry)) 78 | entries))) 79 | (write repo) 80 | (load repo))) 81 | 82 | (defn to-tree-entry [name tree] 83 | (->TreeEntry name (:sha1 tree) :tree)) 84 | -------------------------------------------------------------------------------- /src/gitalin/objects.clj: -------------------------------------------------------------------------------- 1 | (ns gitalin.objects 2 | (:import [java.util UUID] 3 | [org.eclipse.jgit.lib FileMode]) 4 | (:refer-clojure :exclude [load]) 5 | (:require [gitalin.git.coerce :refer [to-oid]] 6 | [gitalin.git.blob :as blob] 7 | [gitalin.git.commit :as commit] 8 | [gitalin.git.tree :as git-tree] 9 | [gitalin.transit :refer [transit-read transit-write]])) 10 | 11 | (defrecord GitalinObject [uuid properties]) 12 | 13 | (defn load-from-entry [repo entry] 14 | (let [uuid (:name entry) 15 | oid (to-oid repo (:sha1 entry)) 16 | blob (blob/load repo oid) 17 | props (transit-read (:data blob))] 18 | (->GitalinObject uuid props))) 19 | 20 | (defn load-all [repo tree] 21 | (some->> tree 22 | :entries 23 | (filter #(= :file (:type %))) 24 | (map #(load-from-entry repo %)))) 25 | 26 | (defn load [repo tree uuid] 27 | (some->> tree 28 | :entries 29 | (filter #(= :file (:type %))) 30 | (filter #(= uuid (:name %))) 31 | (first) 32 | (load-from-entry repo))) 33 | 34 | (defn make [uuid properties] 35 | (->GitalinObject uuid properties)) 36 | 37 | (defn make-blob [repo object] 38 | (blob/write repo (-> (:properties object) 39 | (transit-write) 40 | (.getBytes "utf-8")))) 41 | 42 | (defn to-tree-entry [object blob] 43 | (git-tree/->TreeEntry (:uuid object) 44 | (:sha1 blob) 45 | :file)) 46 | -------------------------------------------------------------------------------- /src/gitalin/protocols.clj: -------------------------------------------------------------------------------- 1 | (ns gitalin.protocols) 2 | 3 | (defprotocol IEntity 4 | (id [this]) 5 | (properties [this])) 6 | 7 | (defprotocol IAdapter 8 | (connect [this]) 9 | (disconnect [this]) 10 | (references [this]) 11 | (reference [this id]) 12 | (commits [this]) 13 | (commit [this id]) 14 | (objects [this]) 15 | (object [this id]) 16 | (transact! [this info mutations])) 17 | 18 | (defprotocol IConnection 19 | (conn-id [this]) 20 | (adapter [this])) 21 | -------------------------------------------------------------------------------- /src/gitalin/query.clj: -------------------------------------------------------------------------------- 1 | (ns gitalin.query 2 | (:import [clojure.lang PersistentVector]) 3 | (:require [clojure.pprint :refer [pprint]] 4 | [clojure.string :as str] 5 | [gitalin.git.coerce :refer [to-oid]] 6 | [gitalin.git.commit :as commit] 7 | [gitalin.git.ident :as ident] 8 | [gitalin.git.repo :as r] 9 | [gitalin.git.reference :as reference] 10 | [gitalin.git.tree :as tree] 11 | [gitalin.objects :as objects] 12 | [gitalin.protocols :as p])) 13 | 14 | ;;;; Debug helpers 15 | 16 | (defmacro debug 17 | [context & body] 18 | `(when (-> ~context :conn :debug) 19 | (println ~@body))) 20 | 21 | (defmacro debug-pprint 22 | [context & body] 23 | `(when (-> ~context :conn :debug) 24 | (pprint ~@body))) 25 | 26 | ;;;; Query representation 27 | 28 | (defrecord Query [find where]) 29 | 30 | (defrecord Variable [symbol]) 31 | (defrecord Constant [value]) 32 | (defrecord Pattern [elements]) 33 | 34 | (defn variable? [x] 35 | (instance? Variable x)) 36 | 37 | (defn constant? [x] 38 | (instance? Constant x)) 39 | 40 | ;;;; Query printing 41 | 42 | (defn variable-str [var] 43 | (when (instance? Variable var) 44 | (:symbol var))) 45 | 46 | (defn constant-str [constant] 47 | (when (instance? Constant constant) 48 | (:value constant))) 49 | 50 | (defn element-str [element] 51 | (or (variable-str element) 52 | (constant-str element))) 53 | 54 | (defn pattern-str [pattern] 55 | {:pre [(instance? Pattern pattern)]} 56 | (str (mapv element-str (:elements pattern)))) 57 | 58 | (defn var-str [var-or-vars] 59 | (if (instance? Variable var-or-vars) 60 | (variable-str var-or-vars) 61 | (mapv var-str var-or-vars))) 62 | 63 | ;;;; Query parsing 64 | 65 | (defn parse-variable [form] 66 | (when (and (symbol? form) 67 | (= (first (name form)) \?)) 68 | (Variable. form))) 69 | 70 | (defn parse-constant [form] 71 | (when-not (symbol? form) 72 | (Constant. form))) 73 | 74 | (defn parse-seq [parse-fn form] 75 | (when (sequential? form) 76 | (reduce #(if-let [res (parse-fn %2)] 77 | (conj %1 res) 78 | (reduced nil)) 79 | [] form))) 80 | 81 | (defn parse-find [form] 82 | (or (parse-variable form) 83 | (parse-seq parse-variable form))) 84 | 85 | (defn parse-in [form] 86 | (let [in (or (parse-variable form) 87 | (parse-seq parse-variable form))] 88 | (cond-> in 89 | (not (sequential? in)) vector))) 90 | 91 | (defn parse-pattern-element [form] 92 | (or (parse-variable form) 93 | (parse-constant form))) 94 | 95 | (defn parse-pattern [form] 96 | (when (vector? form) 97 | (Pattern. (parse-seq parse-pattern-element form)))) 98 | 99 | (defn parse-clauses [form] 100 | (parse-seq parse-pattern form)) 101 | 102 | (defn parse-where [form] 103 | (parse-clauses form)) 104 | 105 | (defn parse-query [q] 106 | (map->Query {:find (parse-find (:find q)) 107 | :in (parse-in (:in q)) 108 | :where (parse-where (:where q))})) 109 | 110 | ;;;; Execution context and variable lookup 111 | 112 | (defrecord Context [conn bindings deps]) 113 | 114 | (defrecord BindingValue [value source-entity]) 115 | 116 | (defn var-bound? [context var] 117 | {:pre [(instance? Context context) 118 | (variable? var)]} 119 | (contains? (:bindings context) var)) 120 | 121 | (defn get-binding [context var] 122 | {:pre [(instance? Context context) 123 | (variable? var)]} 124 | ((:bindings context) var)) 125 | 126 | (defn get-values [context var] 127 | {:pre [(instance? Context context) 128 | (variable? var)]} 129 | (mapv :value (get-binding context var))) 130 | 131 | ;;;; Variable dependencies 132 | 133 | (defn gather-pattern-dependencies [deps pattern] 134 | (when (instance? Pattern pattern) 135 | (let [[id property value] (:elements pattern)] 136 | (if (and (variable? id) 137 | (variable? value)) 138 | (update deps id conj value) 139 | deps)))) 140 | 141 | (defn gather-clause-dependencies [deps clause] 142 | (or (gather-pattern-dependencies deps clause))) 143 | 144 | (defn gather-dependencies [clauses] 145 | {:pre [(vector? clauses)]} 146 | (reduce gather-clause-dependencies {} clauses)) 147 | 148 | ;;;; Query execution 149 | 150 | (defn dispatch-on-property-base [context pattern] 151 | (let [property (second (:elements pattern)) 152 | base (cond-> (:value property) 153 | (keyword? (:value property)) namespace)] 154 | (if (keyword? (:value property)) 155 | (keyword base) 156 | base))) 157 | 158 | (defn has-property? [entity property] 159 | {:pre [(satisfies? p/IEntity entity) 160 | (or (variable? property) 161 | (constant? property))]} 162 | (or (variable? property) 163 | (let [props (p/properties entity)] 164 | (not (empty? (filter #(= (:value property) (first %)) 165 | props)))))) 166 | 167 | (defn gather-property-value 168 | [context entity prop] 169 | {:pre [(satisfies? p/IEntity entity) 170 | (vector? prop) 171 | (= 2 (count prop)) 172 | (keyword? (first prop))]} 173 | (BindingValue. (second prop) entity)) 174 | 175 | (defn gather-entity-values [context entity properties] 176 | {:pre [(instance? Context context) 177 | (satisfies? p/IEntity entity)]} 178 | (let [props (p/properties entity) 179 | props (let [possible-props (map :value properties)] 180 | (filter #(some #{(first %)} possible-props) props))] 181 | (mapv #(gather-property-value context entity %) props))) 182 | 183 | (defn gather-values [context entities prop-or-props] 184 | {:pre [(instance? Context context) 185 | (vector? entities) 186 | (every? #(satisfies? p/IEntity %) entities)]} 187 | (into [] 188 | (apply concat 189 | (mapv #(gather-entity-values context % prop-or-props) 190 | entities)))) 191 | 192 | (defn has-property-value? [entity properties values] 193 | {:pre [(satisfies? p/IEntity entity) 194 | (every? #(instance? BindingValue %) properties) 195 | (every? #(instance? BindingValue %) values)]} 196 | (let [possible-props (map :value properties) 197 | possible-values (map :value values)] 198 | (some (fn [[prop val]] 199 | (and (some #{prop} possible-props) 200 | (some #{val} possible-values))) 201 | (p/properties entity)))) 202 | 203 | (defn gather-entity-properties [context entity] 204 | {:pre [(instance? Context context) 205 | (satisfies? p/IEntity entity)]} 206 | (let [props (p/properties entity)] 207 | (mapv #(BindingValue. (first %) entity) props))) 208 | 209 | (defn gather-properties [context entities] 210 | {:pre [(instance? Context context) 211 | (vector? entities) 212 | (every? #(satisfies? p/IEntity %) entities)]} 213 | (into [] 214 | (apply concat 215 | (mapv #(gather-entity-properties context %) 216 | entities)))) 217 | 218 | (defn update-dependency [new-entities context dep] 219 | (debug context "PATTERN update dependency" (:symbol dep)) 220 | {:pre [(coll? new-entities) 221 | (instance? Context context) 222 | (variable? dep)]} 223 | (if (var-bound? context dep) 224 | (let [values (get-binding context dep) 225 | values-for-entities (filterv (fn [value] 226 | (or (nil? (:source-entity value)) 227 | (some 228 | #{(:source-entity value)} 229 | new-entities))) 230 | values)] 231 | (debug context "PATTERN new values:") 232 | (debug-pprint context values-for-entities) 233 | (-> context 234 | (assoc-in [:bindings dep] values-for-entities))) 235 | context)) 236 | 237 | (defn update-dependencies [context new-entities deps] 238 | {:pre [(instance? Context context) 239 | (coll? new-entities) 240 | (every? variable? deps)]} 241 | (reduce #(update-dependency new-entities %1 %2) context deps)) 242 | 243 | (defn maybe-update-binding [context var values] 244 | {:pre [(instance? Context context) 245 | (every? #(instance? BindingValue %) values)]} 246 | (if (variable? var) 247 | (do 248 | (debug context "PATTERN update binding" (:symbol var)) 249 | (debug context "PATTERN new values:") 250 | (debug-pprint context values) 251 | (let [deps ((:deps context) var) 252 | new-entities (map :source-entity values)] 253 | (-> context 254 | (assoc-in [:bindings var] values) 255 | (update-dependencies new-entities deps)))) 256 | context)) 257 | 258 | (defn resolve-id [context id load-one-fn load-all-fn] 259 | {:pre [(instance? Context context) 260 | (or (constant? id) 261 | (variable? id))]} 262 | (debug context "PATTERN id" (element-str id)) 263 | (debug context "PATTERN id bound?" 264 | (and (variable? id) 265 | (var-bound? context id))) 266 | (debug context "PATTERN id value" 267 | (when (variable? id) 268 | (get-values context id))) 269 | (let [adapter (p/adapter (:conn context))] 270 | (if (variable? id) 271 | (if (var-bound? context id) 272 | (->> (get-values context id) 273 | (mapv #(load-one-fn adapter %))) 274 | (load-all-fn adapter)) 275 | [(load-one-fn adapter (:value id))]))) 276 | 277 | (defn resolve-properties [context property entities] 278 | {:pre [(instance? Context context) 279 | (or (constant? property) 280 | (variable? property))]} 281 | (debug context "PATTERN property" property) 282 | (debug context "PATTERN property bound?" 283 | (and (variable? property) 284 | (var-bound? context property))) 285 | ;; (debug context "PATTERN bindings:") 286 | ;; (debug-pprint context (:bindings context)) 287 | (if (variable? property) 288 | (if (var-bound? context property) 289 | (get-binding context property) 290 | (gather-properties context entities)) 291 | [(BindingValue. (:value property) nil)])) 292 | 293 | (defn resolve-values [context value properties entities] 294 | {:pre [(instance? Context context) 295 | (or (constant? value) 296 | (variable? value)) 297 | (every? #(instance? BindingValue %) properties)]} 298 | (debug context "PATTERN value" value) 299 | (debug context "PATTERN value var bound?" 300 | (and (variable? value) 301 | (var-bound? context value))) 302 | ;; (debug context "PATTERN bindings:") 303 | ;; (debug-pprint context (:bindings context)) 304 | (if (variable? value) 305 | (if (var-bound? context value) 306 | (get-binding context value) 307 | (gather-values context entities properties)) 308 | [(BindingValue. (:value value) nil)])) 309 | 310 | (defn process-pattern* 311 | [context pattern load-one-fn load-all-fn] 312 | (debug context "-------") 313 | (debug context "PATTERN" (pattern-str pattern)) 314 | (debug context "-------") 315 | (let [[id property value] (:elements pattern) 316 | adapter (p/adapter (:conn context)) 317 | ;; Resolve id (var or constant) into entities 318 | entities (resolve-id context id load-one-fn load-all-fn) 319 | _ (debug context "PATTERN entities") 320 | _ (debug-pprint context entities) 321 | ;; Drop entities that don't have the property (var or constant) 322 | entities (filterv #(has-property? % property) entities) 323 | _ (debug context "PATTERN entities with" (element-str property)) 324 | _ (debug-pprint context entities) 325 | ;; Gather allowed properties 326 | properties (resolve-properties context property entities) 327 | _ (debug context "PATTERN properties") 328 | _ (debug-pprint context properties) 329 | ;; Gather allowed values 330 | values (resolve-values context value properties entities) 331 | _ (debug context "PATTERN values") 332 | _ (debug-pprint context values) 333 | ;; Drop entities that don't match the allowed values 334 | entities (filterv #(has-property-value? % properties values) 335 | entities) 336 | _ (debug context "PATTERN entities with matching properties") 337 | _ (debug-pprint context entities) 338 | entity-values (mapv #(BindingValue. (:id %) %) entities)] 339 | (-> context 340 | (maybe-update-binding id entity-values) 341 | (maybe-update-binding property properties) 342 | (maybe-update-binding value values)))) 343 | 344 | (defmulti process-pattern dispatch-on-property-base) 345 | 346 | (defmethod process-pattern :ref 347 | [context pattern] 348 | (process-pattern* context pattern p/reference p/references)) 349 | 350 | (defmethod process-pattern :commit 351 | [context pattern] 352 | (process-pattern* context pattern p/commit p/commits)) 353 | 354 | (defn process-object-pattern 355 | [context pattern] 356 | (process-pattern* context pattern p/object p/objects)) 357 | 358 | (defmethod process-pattern :object 359 | [context pattern] 360 | (process-object-pattern context pattern)) 361 | 362 | (defmethod process-pattern :default 363 | [context pattern] 364 | (process-object-pattern context pattern)) 365 | 366 | (defn process-pattern-clause [context clause] 367 | (when (instance? Pattern clause) 368 | (process-pattern context clause))) 369 | 370 | (defn process-clause [context clause] 371 | (or (process-pattern-clause context clause))) 372 | 373 | ;;;; Collecting query results 374 | 375 | (defn collect [var-or-vars context] 376 | (debug context "COLLECT" (var-str var-or-vars)) 377 | (if (variable? var-or-vars) 378 | (let [val (into #{} (get-values context var-or-vars))] 379 | (debug context "COLLECT val") 380 | (debug-pprint context val) 381 | val) 382 | (if (coll? var-or-vars) 383 | (let [vals (mapv #(get-values context %) var-or-vars)] 384 | (assert (apply = (map count vals))) 385 | (debug context "COLLECT vals") 386 | (debug-pprint context vals) 387 | (into #{} (apply mapv vector vals)))))) 388 | 389 | ;;;; Entry point 390 | 391 | (defn q [conn q args] 392 | {:pre [(satisfies? p/IConnection conn) 393 | (map? q)]} 394 | (debug {:conn conn}) 395 | (debug {:conn conn} "q" q args) 396 | (let [q (parse-query q) 397 | ins (zipmap (:in q) 398 | (mapv #(vector (BindingValue. % nil)) args)) 399 | deps (gather-dependencies (:where q)) 400 | _ (debug {:conn conn} "dependencies") 401 | _ (debug-pprint {:conn conn} deps) 402 | results (reduce process-clause 403 | (Context. conn ins deps) 404 | (:where q))] 405 | (debug results "RESULTS") 406 | (debug-pprint results results) 407 | (collect (:find q) results))) 408 | -------------------------------------------------------------------------------- /src/gitalin/transit.clj: -------------------------------------------------------------------------------- 1 | (ns gitalin.transit 2 | (:require [cognitect.transit :as transit]) 3 | (:import [java.io ByteArrayInputStream ByteArrayOutputStream])) 4 | 5 | (defn transit-write 6 | [data] 7 | (let [stream (ByteArrayOutputStream. 4096)] 8 | (-> stream 9 | (transit/writer :json) 10 | (transit/write data)) 11 | (.toString stream))) 12 | 13 | (defn transit-read 14 | [data] 15 | (let [stream (ByteArrayInputStream. data)] 16 | (-> stream 17 | (transit/reader :json) 18 | (transit/read)))) 19 | -------------------------------------------------------------------------------- /test/gitalin/test/core.clj: -------------------------------------------------------------------------------- 1 | (ns gitalin.test.core 2 | (:require [clojure.test.check.clojure-test :refer [defspec]] 3 | [clojure.test.check.generators :as gen] 4 | [clojure.test.check.properties :as prop] 5 | [gitalin.core :as c] 6 | [gitalin.protocols :as p] 7 | [gitalin.test.setup :as setup :refer [with-conn]])) 8 | 9 | (defspec connection-is-constructed-correctly 10 | (prop/for-all [adapter setup/gen-store] 11 | (with-conn (c/connect adapter) 12 | (and (c/connection? conn) 13 | (satisfies? p/IConnection conn) 14 | (satisfies? p/IAdapter (p/adapter conn)))))) 15 | -------------------------------------------------------------------------------- /test/gitalin/test/query.clj: -------------------------------------------------------------------------------- 1 | (ns gitalin.test.query 2 | (:import [gitalin.core Connection]) 3 | (:require [clojure.pprint :refer [pprint]] 4 | [clojure.test :refer [is]] 5 | [clojure.test.check.clojure-test :refer [defspec]] 6 | [clojure.test.check.generators :as gen] 7 | [clojure.test.check.properties :as prop] 8 | [gitalin.test.setup :as setup :refer [with-conn]] 9 | [gitalin.test.transact :refer [object-add?]] 10 | [gitalin.core :as c] 11 | [gitalin.query :as q] 12 | [gitalin.protocols :as p] 13 | [gitalin.adapter :as a])) 14 | 15 | ;;;; Reference queries 16 | 17 | (defspec querying-refs-after-empty-transactions-returns-nothing 10 18 | (prop/for-all [store setup/gen-store 19 | transactions setup/gen-add-transactions] 20 | (with-conn (assoc (c/connect store) :debug false) 21 | (doseq [{:keys [info data]} transactions] 22 | (c/transact! conn info data)) 23 | (or (not (empty? transactions)) 24 | (and (is (= #{} 25 | (c/q conn '{:find ?n 26 | :where [[?ref :ref/name ?n]]}))) 27 | (is (= #{} 28 | (c/q conn '{:find ?ref 29 | :where [[?ref :ref/name "HEAD"]]}))) 30 | (is (= #{} 31 | (c/q conn '{:find ?t 32 | :where [[?ref :ref/type ?t]]}))) 33 | (is (= #{} 34 | (c/q conn '{:find ?c 35 | :where [[?ref :ref/commit ?c]]})))))))) 36 | 37 | (defspec ref-names-can-be-queried 10 38 | (prop/for-all [store setup/gen-store 39 | transactions setup/gen-add-transactions] 40 | (with-conn (assoc (c/connect store) :debug false) 41 | (doseq [{:keys [info data]} transactions] 42 | (c/transact! conn info data)) 43 | (or (empty? transactions) 44 | (is (= #{"HEAD" "refs:heads:master"} 45 | (c/q conn '{:find ?n 46 | :where [[?ref :ref/name ?n]]}))))))) 47 | 48 | (defspec ref-ids-can-be-queried 10 49 | (prop/for-all [store setup/gen-store 50 | transactions setup/gen-add-transactions] 51 | (with-conn (assoc (c/connect store) :debug false) 52 | (doseq [{:keys [info data]} transactions] 53 | (c/transact! conn info data)) 54 | (or (empty? transactions) 55 | (is (= #{"reference/HEAD" "reference/refs:heads:master"} 56 | (c/q conn '{:find ?ref 57 | :where [[?ref :ref/name ?n]]}))))))) 58 | 59 | (defspec ref-types-can-be-queried 10 60 | (prop/for-all [store setup/gen-store 61 | transactions setup/gen-add-transactions] 62 | (with-conn (assoc (c/connect store) :debug false) 63 | (doseq [{:keys [info data]} transactions] 64 | (c/transact! conn info data)) 65 | (or (empty? transactions) 66 | (is (= #{"branch"} 67 | (c/q conn '{:find ?t 68 | :where [[?ref :ref/type ?t]]}))))))) 69 | 70 | (defspec ref-commits-can-be-queried 10 71 | (prop/for-all [store setup/gen-store 72 | transactions setup/gen-add-transactions] 73 | (with-conn (assoc (c/connect store) :debug false) 74 | (doseq [{:keys [info data]} transactions] 75 | (c/transact! conn info data)) 76 | (or (empty? transactions) 77 | (and 78 | (is (= 1 79 | (count 80 | (c/q conn '{:find ?c 81 | :where [[?ref :ref/commit ?c]]})))) 82 | (is (re-matches 83 | #"commit/[0-9abcdef]{40}" 84 | (first 85 | (c/q conn '{:find ?c 86 | :where [[?ref :ref/commit ?c]]}))))))))) 87 | 88 | (defspec multiple-refs-properties-can-be-queried-at-once 10 89 | (prop/for-all [store setup/gen-store 90 | transactions setup/gen-add-transactions] 91 | (with-conn (assoc (c/connect store) :debug false) 92 | (doseq [{:keys [info data]} transactions] 93 | (c/transact! conn info data)) 94 | (or (empty? transactions) 95 | (is (= #{["HEAD" 96 | "reference/HEAD"] 97 | ["refs:heads:master" 98 | "reference/refs:heads:master"]} 99 | (c/q conn '{:find [?name ?ref] 100 | :where [[?ref :ref/name ?name]]}))))))) 101 | 102 | (defspec ref-names-can-be-parameterized 10 103 | (prop/for-all [store setup/gen-store 104 | transactions setup/gen-add-transactions] 105 | (with-conn (assoc (c/connect store) :debug false) 106 | (doseq [{:keys [info data]} transactions] 107 | (c/transact! conn info data)) 108 | (or (empty? transactions) 109 | (is (= #{"reference/HEAD"} 110 | (c/q conn '{:find ?ref 111 | :in ?name 112 | :where [[?ref :ref/name ?name]]} 113 | "HEAD"))))))) 114 | 115 | (defspec ref-ids-can-be-parameterized 10 116 | (prop/for-all [store setup/gen-store 117 | transactions setup/gen-add-transactions] 118 | (with-conn (assoc (c/connect store) :debug false) 119 | (doseq [{:keys [info data]} transactions] 120 | (c/transact! conn info data)) 121 | (or (empty? transactions) 122 | (is (= #{"HEAD"} 123 | (c/q conn '{:find ?name 124 | :in ?ref 125 | :where [[?ref :ref/name ?name]]} 126 | "reference/HEAD"))))))) 127 | 128 | (defspec ref-types-can-be-parameterized 10 129 | (prop/for-all [store setup/gen-store 130 | transactions setup/gen-add-transactions] 131 | (with-conn (assoc (c/connect store) :debug false) 132 | (doseq [{:keys [info data]} transactions] 133 | (c/transact! conn info data)) 134 | (or (empty? transactions) 135 | (is (= #{"reference/HEAD" "reference/refs:heads:master"} 136 | (c/q conn '{:find ?ref 137 | :in ?type 138 | :where [[?ref :ref/type ?type]]} 139 | "branch"))))))) 140 | 141 | ;;;; Commit queries 142 | 143 | (defspec querying-commits-after-empty-transactions-returns-nothing 10 144 | (prop/for-all [store setup/gen-store 145 | transactions setup/gen-add-transactions] 146 | (with-conn (assoc (c/connect store) :debug false) 147 | (doseq [{:keys [info data]} transactions] 148 | (c/transact! conn info data)) 149 | (or (not (empty? transactions)) 150 | (and (is (= #{} 151 | (c/q conn 152 | '{:find ?s 153 | :where [[?commit :commit/sha1 ?s]]}))) 154 | (is (= #{} 155 | (c/q conn 156 | '{:find ?a 157 | :where [[?commit :commit/author ?a]]}))) 158 | (is (= #{} 159 | (c/q conn 160 | '{:find ?c 161 | :where [[?commit :commit/committer ?c]]}))) 162 | (is (= #{} 163 | (c/q conn 164 | '{:find ?m 165 | :where [[?commit :commit/message ?m]]}))) 166 | (is (= #{} 167 | (c/q conn 168 | '{:find ?o 169 | :where [[?commit :commit/object ?o]]}))) 170 | (is (= #{} 171 | (c/q conn 172 | '{:find ?p 173 | :where [[?commit :commit/parent ?p]]})))))))) 174 | 175 | (defspec commit-sha1s-can-be-queried 10 176 | (prop/for-all [store setup/gen-store 177 | transactions setup/gen-add-transactions] 178 | (with-conn (assoc (c/connect store) :debug false) 179 | (doseq [{:keys [info data]} transactions] 180 | (c/transact! conn info data)) 181 | (or (empty? transactions) 182 | (let [sha1s (c/q conn '{:find ?s 183 | :where [[?c :commit/sha1 ?s]]})] 184 | (and (is (set? sha1s)) 185 | (is (every? #(re-matches #"[0-9abcdef]{40}" %) 186 | sha1s)))))))) 187 | 188 | (defspec commit-authors-can-be-queried 10 189 | (prop/for-all [store setup/gen-store 190 | transactions setup/gen-add-transactions] 191 | (with-conn (assoc (c/connect store) :debug false) 192 | (doseq [{:keys [info data]} transactions] 193 | (c/transact! conn info data)) 194 | (or (empty? transactions) 195 | (let [expected-authors (into #{} 196 | (comp (map :info) 197 | (map :author)) 198 | transactions) 199 | authors (c/q conn '{:find ?a 200 | :where [[?c :commit/author ?a]]}) 201 | authors (into #{} 202 | (map #(select-keys % [:name :email])) 203 | authors)] 204 | (is (= expected-authors authors))))))) 205 | 206 | (defspec commit-committers-can-be-queried 10 207 | (prop/for-all [store setup/gen-store 208 | transactions setup/gen-add-transactions] 209 | (with-conn (assoc (c/connect store) :debug false) 210 | (doseq [{:keys [info data]} transactions] 211 | (c/transact! conn info data)) 212 | (or (empty? transactions) 213 | (let [expected-committers (into #{} 214 | (comp (map :info) 215 | (map :committer)) 216 | transactions) 217 | committers (c/q conn 218 | '{:find ?cm 219 | :where [[?c :commit/committer ?cm]]}) 220 | committers (into #{} 221 | (map #(select-keys % [:name :email])) 222 | committers)] 223 | (is (= expected-committers committers))))))) 224 | 225 | (defspec commit-messages-can-be-queried 10 226 | (prop/for-all [store setup/gen-store 227 | transactions setup/gen-add-transactions] 228 | (with-conn (assoc (c/connect store) :debug false) 229 | (doseq [{:keys [info data]} transactions] 230 | (c/transact! conn info data)) 231 | (or (empty? transactions) 232 | (let [expected-messages (into #{} 233 | (comp (map :info) 234 | (map :message)) 235 | transactions) 236 | messages (c/q conn 237 | '{:find ?msg 238 | :where [[?c :commit/message ?msg]]})] 239 | (and (is (set? messages)) 240 | (is (= expected-messages messages)))))))) 241 | 242 | (defspec commit-parents-can-be-queried 10 243 | (prop/for-all [store setup/gen-store 244 | transactions setup/gen-add-transactions] 245 | (with-conn (assoc (c/connect store) :debug false) 246 | (doseq [{:keys [info data]} transactions] 247 | (c/transact! conn info data)) 248 | (or (empty? transactions) 249 | (let [parents (c/q conn 250 | '{:find ?p 251 | :where [[?c :commit/parent ?p]]})] 252 | (and (is (= (- (count transactions) 1) 253 | (count parents))) 254 | (is (every? #(re-matches #"commit/[0-9abcdef]{40}" %) 255 | parents)))))))) 256 | 257 | (defspec commit-objects-can-be-queried 10 258 | (prop/for-all [store setup/gen-store 259 | transactions setup/gen-add-transactions] 260 | (with-conn (assoc (c/connect store) :debug false) 261 | (doseq [{:keys [info data]} transactions] 262 | (c/transact! conn info data)) 263 | (or (empty? transactions) 264 | (let [objects (c/q conn 265 | '{:find ?o 266 | :where [[?c :commit/objects ?o]]})] 267 | (is (every? #(re-matches #"object/[0-9acbdef]{40}([a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}){1}" %) 268 | objects))))))) 269 | 270 | ;;;; Object queries 271 | 272 | (defspec querying-objects-after-empty-transactions-returns-nothing 10 273 | (prop/for-all [store setup/gen-store 274 | transactions setup/gen-add-transactions] 275 | (with-conn (assoc (c/connect store) :debug false) 276 | (doseq [{:keys [info data]} transactions] 277 | (c/transact! conn info data)) 278 | (or (not (empty? transactions)) 279 | (and (is (= #{} 280 | (c/q conn 281 | '{:find ?u 282 | :where [[?object :object/uuid ?u]]}))) 283 | (is (= #{} 284 | (c/q conn 285 | '{:find ?c 286 | :where [[?o :object/commit ?c]]})))))))) 287 | 288 | (defspec object-uuids-can-be-queried 10 289 | (prop/for-all [store setup/gen-store 290 | transactions setup/gen-add-transactions] 291 | (with-conn (assoc (c/connect store) :debug false) 292 | (doseq [{:keys [info data]} transactions] 293 | (c/transact! conn info data)) 294 | (or (empty? transactions) 295 | (let [expected-uuids (->> transactions 296 | (map :data) 297 | (map (fn [t] (map second t))) 298 | (apply concat) 299 | (into #{})) 300 | uuids (c/q conn '{:find ?u 301 | :where [[?o :object/uuid ?u]]})] 302 | (is (= expected-uuids uuids))))))) 303 | 304 | (defspec object-commits-can-be-queried 10 305 | (prop/for-all [store setup/gen-store 306 | transactions setup/gen-add-transactions] 307 | (with-conn (assoc (c/connect store) :debug false) 308 | (doseq [{:keys [info data]} transactions] 309 | (c/transact! conn info data)) 310 | (or (empty? transactions) 311 | (let [commits (c/q conn '{:find ?c 312 | :where [[?o :object/commit ?c]]})] 313 | (and (is (set? commits)) 314 | (is (every? #(re-matches #"commit/[0-9abcdef]{40}" %) 315 | commits)))))))) 316 | 317 | ;; TODO: 318 | ;; * Tests for querying object properties 319 | -------------------------------------------------------------------------------- /test/gitalin/test/setup.clj: -------------------------------------------------------------------------------- 1 | (ns gitalin.test.setup 2 | (:require [clojure.java.io :as io] 3 | [clojure.test.check.clojure-test :refer [defspec]] 4 | [clojure.test.check.generators :as gen] 5 | [clojure.test.check.properties :as prop] 6 | [me.raynes.fs :as fs] 7 | [gitalin.git.repo :as repo] 8 | [gitalin.adapter :as a] 9 | [gitalin.core :as c])) 10 | 11 | ;;;; Misc helpers 12 | 13 | (defmacro dofor 14 | [bindings & body] 15 | `(doall 16 | (for ~bindings 17 | ~@body))) 18 | 19 | ;;;; Create temporary directories 20 | 21 | (def gen-temp-dir 22 | (gen/fmap (fn [some-int] (fs/temp-dir "gitalin")) gen/int)) 23 | 24 | (defspec temp-dir-generator-works 10 25 | (prop/for-all [dir gen-temp-dir] 26 | (try 27 | (fs/directory? dir) 28 | (finally (fs/delete-dir dir))))) 29 | 30 | ;;;; Create temporary stores 31 | 32 | (defn init-store [dir] 33 | (let [path (.getAbsolutePath dir)] 34 | (c/default-adapter (c/create-store! path)))) 35 | 36 | (def gen-store 37 | (gen/fmap init-store gen-temp-dir)) 38 | 39 | (defn create-store [] 40 | (-> (fs/temp-dir "gitalin") 41 | init-store)) 42 | 43 | (defn delete-store [path] 44 | (fs/delete-dir (io/as-file path))) 45 | 46 | (defn create-store-with-conn [] 47 | (-> (fs/temp-dir "gitalin") 48 | init-store 49 | c/connect)) 50 | 51 | (defn delete-store-from-conn [conn] 52 | (delete-store (:path (c/adapter conn)))) 53 | 54 | ;;;; Use connections in specs 55 | 56 | (defmacro with-conn 57 | [conn & body] 58 | `(let [~(symbol "conn") ~conn] 59 | (try 60 | (do 61 | ~@body) 62 | (finally 63 | (delete-store-from-conn ~(symbol "conn")))))) 64 | 65 | ;;;; Generate transactions 66 | 67 | (def gen-transaction-info-HEAD 68 | (gen/hash-map 69 | :target (gen/return "HEAD") 70 | :author (gen/hash-map :name gen/string-alphanumeric 71 | :email gen/string-alphanumeric) 72 | :committer (gen/hash-map :name gen/string-alphanumeric 73 | :email gen/string-alphanumeric) 74 | :message gen/string)) 75 | 76 | (def gen-add-mutations 77 | (gen/vector-distinct 78 | (gen/fmap vec 79 | (gen/tuple (gen/return :object/add) 80 | (gen/fmap str gen/uuid) 81 | gen/keyword 82 | gen/any)))) 83 | 84 | (def gen-add-transactions 85 | (gen/vector-distinct 86 | (gen/hash-map :info gen-transaction-info-HEAD 87 | :data gen-add-mutations))) 88 | 89 | (def gen-tempid 90 | (gen/fmap (fn [_] (a/tempid)) gen/int)) 91 | 92 | (def gen-add-tempid-mutations 93 | (gen/vector-distinct 94 | (gen/fmap vec 95 | (gen/tuple (gen/return :object/add) 96 | gen-tempid 97 | gen/keyword 98 | gen/any)))) 99 | 100 | (def gen-tempid-add-transactions 101 | (gen/vector-distinct 102 | (gen/hash-map :info gen-transaction-info-HEAD 103 | :data gen-add-tempid-mutations))) 104 | 105 | (defn gen-add-mutation-for-uuid [uuid] 106 | (gen/fmap vec 107 | (gen/tuple (gen/return :object/add) 108 | (gen/return uuid) 109 | gen/keyword 110 | gen/any))) 111 | 112 | (defn gen-set-mutation-for-uuid [uuid] 113 | (gen/fmap vec 114 | (gen/tuple (gen/return :object/set) 115 | (gen/return uuid) 116 | gen/keyword 117 | gen/any))) 118 | 119 | (defn gen-set-mutations-for-uuid [uuid] 120 | (gen/vector 121 | (gen/bind (gen/return uuid) 122 | gen-set-mutation-for-uuid))) 123 | 124 | (defn gen-add-set-mutations-for-uuid [uuid] 125 | (gen/fmap (fn [[add sets]] 126 | (into [] (concat [add] sets))) 127 | (gen/tuple 128 | (gen/bind (gen/return uuid) 129 | gen-add-mutation-for-uuid) 130 | (gen/bind (gen/return uuid) 131 | gen-set-mutations-for-uuid)))) 132 | 133 | (def gen-add-set-mutations 134 | (gen/fmap (fn [mutation-groups] 135 | (into [] (apply concat mutation-groups))) 136 | (gen/vector-distinct 137 | (gen/bind gen-tempid 138 | gen-add-set-mutations-for-uuid)))) 139 | 140 | (def gen-add-set-transactions 141 | (gen/vector-distinct 142 | (gen/hash-map :info gen-transaction-info-HEAD 143 | :data gen-add-set-mutations))) 144 | 145 | (defn gen-unset-mutation-for-uuid [uuid] 146 | (gen/fmap vec 147 | (gen/tuple (gen/return :object/unset) 148 | (gen/return uuid) 149 | gen/keyword 150 | gen/any))) 151 | 152 | (defn gen-unset-mutations-for-uuid [uuid] 153 | (gen/vector 154 | (gen/bind (gen/return uuid) 155 | gen-unset-mutation-for-uuid))) 156 | 157 | (defn gen-add-set-unset-mutations-for-uuid [uuid] 158 | (gen/fmap (fn [[add sets]] 159 | (into [] (concat [add] sets))) 160 | (gen/tuple 161 | (gen/bind (gen/return uuid) 162 | gen-add-mutation-for-uuid) 163 | (gen/bind (gen/return uuid) 164 | gen-set-mutations-for-uuid) 165 | (gen/bind (gen/return uuid) 166 | gen-unset-mutations-for-uuid)))) 167 | 168 | (def gen-add-set-unset-mutations 169 | (gen/fmap (fn [mutation-groups] 170 | (into [] (apply concat mutation-groups))) 171 | (gen/vector-distinct 172 | (gen/bind gen-tempid 173 | gen-add-set-unset-mutations-for-uuid)))) 174 | 175 | (def gen-add-set-unset-transactions 176 | (gen/vector-distinct 177 | (gen/hash-map :info gen-transaction-info-HEAD 178 | :data gen-add-set-unset-mutations))) 179 | -------------------------------------------------------------------------------- /test/gitalin/test/transact.clj: -------------------------------------------------------------------------------- 1 | (ns gitalin.test.transact 2 | (:import [gitalin.core Connection]) 3 | (:require [clojure.pprint :refer [pprint]] 4 | [clojure.test :refer [is]] 5 | [clojure.test.check.clojure-test :refer [defspec]] 6 | [clojure.test.check.generators :as gen] 7 | [clojure.test.check.properties :as prop] 8 | [gitalin.test.setup :as setup :refer [with-conn]] 9 | [gitalin.core :as c] 10 | [gitalin.query :as q] 11 | [gitalin.protocols :as p] 12 | [gitalin.adapter :as a])) 13 | 14 | (defspec as-many-commits-created-as-transactions 10 15 | (prop/for-all [store setup/gen-store 16 | transactions setup/gen-add-transactions] 17 | (with-conn (assoc (c/connect store) :debug false) 18 | (doseq [{:keys [info data]} transactions] 19 | (c/transact! conn info data)) 20 | (let [res (c/q conn '{:find ?c 21 | :where [[?c :commit/sha1 ?s]]})] 22 | (is (= (count transactions) 23 | (count res))))))) 24 | 25 | (defspec each-commit-correspond-to-one-transaction 10 26 | (prop/for-all [store setup/gen-store 27 | transactions setup/gen-add-transactions] 28 | (with-conn (assoc (c/connect store) :debug false) 29 | (doseq [{:keys [info data]} transactions] 30 | (c/transact! conn info data)) 31 | (is (= (into #{} (map #(:message (:info %)) transactions)) 32 | (c/q conn '{:find ?msg 33 | :where [[?c :commit/message ?msg]]})))))) 34 | 35 | (defspec transactions-update-HEAD 10 36 | (prop/for-all [store setup/gen-store 37 | txs setup/gen-add-transactions] 38 | (with-conn (assoc (c/connect store) :debug false) 39 | (every? true? 40 | (for [{:keys [info data]} txs] 41 | (do 42 | (c/transact! conn info data) 43 | (is (= #{(:message info)} 44 | (c/q conn '{:find ?msg 45 | :where 46 | [[?ref :ref/name "HEAD"] 47 | [?ref :ref/commit ?commit] 48 | [?commit :commit/message ?msg]]}))))))))) 49 | 50 | (defspec transactions-update-master 10 51 | (prop/for-all [store setup/gen-store 52 | txs setup/gen-add-transactions] 53 | (with-conn (assoc (c/connect store) :debug false) 54 | (every? true? 55 | (for [{:keys [info data]} txs] 56 | (do 57 | (c/transact! conn info data) 58 | (is (= #{(:message info)} 59 | (c/q conn '{:find ?msg 60 | :where 61 | [[?ref :ref/name "refs:heads:master"] 62 | [?ref :ref/commit ?commit] 63 | [?commit :commit/message ?msg]]}))))))))) 64 | 65 | (defspec HEAD-and-master-point-to-the-same-commit 10 66 | (prop/for-all [store setup/gen-store 67 | txs setup/gen-add-transactions] 68 | (with-conn (assoc (c/connect store) :debug false) 69 | (every? true? 70 | (for [{:keys [info data]} txs] 71 | (do 72 | (c/transact! conn info data) 73 | (is (= (c/q conn '{:find ?h 74 | :where 75 | [[?ref :ref/name "HEAD"] 76 | [?ref :ref/commit ?h]]}) 77 | (c/q conn '{:find ?m 78 | :where 79 | [[?ref :ref/name "refs:heads:master"] 80 | [?ref :ref/commit ?m]]}))))))))) 81 | 82 | (defn object-add? [mutation] 83 | (and (sequential? mutation) 84 | (= :object/add (first mutation)))) 85 | 86 | (defn get-object-additions [txs] 87 | (->> txs 88 | (map :data) 89 | (map #(filter object-add? %)) 90 | (apply concat))) 91 | 92 | (defspec each-created-object-exists-in-the-corresponding-transaction 10 93 | (prop/for-all [store setup/gen-store 94 | txs setup/gen-add-transactions] 95 | (with-conn (assoc (c/connect store) :debug false) 96 | (doseq [{:keys [info data]} txs] 97 | (c/transact! conn info data)) 98 | (let [additions (get-object-additions txs) 99 | data (map (fn [[_ uuid prop val]] 100 | [uuid prop val]) 101 | additions) 102 | uuids-and-values (map (fn [[uuid _ val]] 103 | [uuid val]) 104 | data) 105 | result (apply concat 106 | (for [d data] 107 | (let [[_ prop _] d] 108 | (c/q conn 109 | `{:find [?u ?v] 110 | :where [[?ref :ref/name "HEAD"] 111 | [?ref :ref/commit ?commit] 112 | [?commit :commit/object ?o] 113 | [?o :object/uuid ?u] 114 | [?o ~prop ?v]]}))))] 115 | (is (= (into #{} uuids-and-values) 116 | (into #{} result))))))) 117 | 118 | (defspec all-created-objects-are-still-present-at-the-end 10 119 | (prop/for-all [store setup/gen-store 120 | txs setup/gen-add-transactions] 121 | (with-conn (assoc (c/connect store) :debug false) 122 | (doseq [{:keys [info data]} txs] 123 | (c/transact! conn info data)) 124 | (let [uuids (->> (get-object-additions txs) 125 | (map (fn [[_ uuid _]] uuid)))] 126 | (is (= (into #{} uuids) 127 | (into #{} (c/q conn 128 | '{:find ?u 129 | :where [[?ref :ref/name "HEAD"] 130 | [?ref :ref/commit ?commit] 131 | [?commit :commit/object ?o] 132 | [?o :object/uuid ?u]]})))))))) 133 | 134 | (defspec temporary-ids-are-translated-to-real-ones 10 135 | (prop/for-all [store setup/gen-store 136 | txs setup/gen-tempid-add-transactions] 137 | (with-conn (assoc (c/connect store) :debug false) 138 | (doseq [{:keys [info data]} txs] 139 | (c/transact! conn info data)) 140 | (is (not-any? #(a/tempid? %) 141 | (c/q conn '{:find ?u 142 | :where [[?o :object/uuid ?u]]})))))) 143 | 144 | (defspec temporary-ids-are-translated-to-real-ones 10 145 | (prop/for-all [store setup/gen-store 146 | txs setup/gen-tempid-add-transactions] 147 | (with-conn (assoc (c/connect store) :debug false) 148 | (doseq [{:keys [info data]} txs] 149 | (c/transact! conn info data)) 150 | (is (not-any? #(a/tempid? %) 151 | (c/q conn '{:find ?u 152 | :where [[?o :object/uuid ?u]]})))))) 153 | 154 | (defn group-mutations-by-uuid [transactions] 155 | (let [mutations (apply concat (map :data transactions))] 156 | (group-by second mutations))) 157 | 158 | (defn collect-prop-val [res mutation] 159 | (let [prop (mutation 2) 160 | val (mutation 3)] 161 | (condp = (first mutation) 162 | :object/add (assoc res prop val) 163 | :object/set (assoc res prop val) 164 | :object/unset (dissoc res prop)))) 165 | 166 | (defn collect-uuid-prop-vals [mutations-by-uuid] 167 | (into {} 168 | (map (fn [[uuid mutations]] 169 | [(cond-> uuid (a/tempid? uuid) :uuid) 170 | (into #{} 171 | (map vec) 172 | (reduce collect-prop-val 173 | {} 174 | mutations))])) 175 | mutations-by-uuid)) 176 | 177 | (defn query-prop-vals-for-uuids [conn uuids] 178 | (let [prop-vals (map #(vector % 179 | (c/q conn 180 | '{:find [?p ?v] 181 | :in ?u 182 | :where [[?ref :ref/name "HEAD"] 183 | [?ref :ref/commit ?c] 184 | [?c :commit/object ?o] 185 | [?o :object/uuid ?u] 186 | [?o ?p ?v]]} 187 | %)) 188 | uuids)] 189 | (into {} 190 | (map (fn [[uuid props]] 191 | [uuid 192 | (into #{} 193 | (filter #(condp = (first %) 194 | :object/uuid false 195 | :object/commit false 196 | %) 197 | props))])) 198 | prop-vals))) 199 | 200 | (defspec sets-after-additions-change-objects-as-expected 10 201 | (prop/for-all [store setup/gen-store 202 | txs setup/gen-add-set-transactions] 203 | (with-conn (assoc (c/connect store) :debug false) 204 | (doseq [{:keys [info data]} txs] 205 | (c/transact! conn info data)) 206 | (let [mutations-by-uuid (group-mutations-by-uuid txs) 207 | expected-uuids (into #{} 208 | (map #(cond-> % (a/tempid? %) :uuid)) 209 | (keys mutations-by-uuid)) 210 | expected-prop-vals (collect-uuid-prop-vals mutations-by-uuid) 211 | actual-prop-vals (query-prop-vals-for-uuids conn 212 | expected-uuids)] 213 | (and (is (= expected-uuids 214 | (c/q conn '{:find ?u 215 | :where [[?ref :ref/name "HEAD"] 216 | [?ref :ref/commit ?commit] 217 | [?commit :commit/object ?o] 218 | [?o :object/uuid ?u]]}))) 219 | (is (= expected-prop-vals 220 | actual-prop-vals))))))) 221 | 222 | (defspec unsets-after-sets-unset-properties-as-expected 10 223 | (prop/for-all [store setup/gen-store 224 | txs setup/gen-add-set-unset-transactions] 225 | (with-conn (assoc (c/connect store) :debug false) 226 | (doseq [{:keys [info data]} txs] 227 | (c/transact! conn info data)) 228 | (let [mutations-by-uuid (group-mutations-by-uuid txs) 229 | expected-uuids (into #{} 230 | (map #(cond-> % (a/tempid? %) :uuid)) 231 | (keys mutations-by-uuid)) 232 | expected-prop-vals (collect-uuid-prop-vals mutations-by-uuid) 233 | actual-prop-vals (query-prop-vals-for-uuids conn 234 | expected-uuids)] 235 | (and (is (= expected-uuids 236 | (c/q conn '{:find ?u 237 | :where [[?ref :ref/name "HEAD"] 238 | [?ref :ref/commit ?commit] 239 | [?commit :commit/object ?o] 240 | [?o :object/uuid ?u]]}))) 241 | (is (= expected-prop-vals 242 | actual-prop-vals))))))) 243 | --------------------------------------------------------------------------------