├── .project ├── .smalltalk.ston ├── .travis.yml ├── LICENSE ├── README.md ├── figures ├── corrupted-version.png ├── package-ancestry.png ├── project-ancestry-labels.png ├── project-ancestry.png ├── subset-packages-ancestry.png └── testing-source.png └── repository ├── .properties ├── BaselineOfGitMigration ├── BaselineOfGitMigration.class.st └── package.st └── GitMigration ├── GitMigration.class.st ├── GitMigrationAuthor.class.st ├── GitMigrationAuthorMapping.class.st ├── GitMigrationAuthorMappingTest.class.st ├── GitMigrationCommitInfo.class.st ├── GitMigrationCommitInfoTest.class.st ├── GitMigrationFastImportWriter.class.st ├── GitMigrationFastImportWriterTest.class.st ├── GitMigrationFileTreeWriter.class.st ├── GitMigrationFileTreeWriterTest.class.st ├── GitMigrationMemoryTreeGitRepository.class.st ├── GitMigrationTest.class.st ├── GitMigrationTonelWriter.class.st ├── GitMigrationTonelWriterTest.class.st ├── GitMigrationVisualization.class.st ├── TonelWriteError.class.st ├── TonelWriter.extension.st └── package.st /.project: -------------------------------------------------------------------------------- 1 | { 2 | 'srcDirectory' : 'repository' 3 | } -------------------------------------------------------------------------------- /.smalltalk.ston: -------------------------------------------------------------------------------- 1 | SmalltalkCISpec { 2 | #loading : [ 3 | SCIMetacelloLoadSpec { 4 | #baseline : 'GitMigration', 5 | #directory : 'repository', 6 | #platforms : [ #pharo ] 7 | } 8 | ], 9 | #testing : { 10 | #packages : [ 'GitMigration' ], 11 | #coverage : { 12 | #packages : [ 'GitMigration' ] 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: smalltalk 2 | sudo: false 3 | 4 | os: 5 | - linux 6 | 7 | smalltalk: 8 | - Pharo-7.0 9 | 10 | cache: 11 | directories: 12 | $HOME/package-cache 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Peter Uhnak 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # **REPO MIGRATED TO https://github.com/pharo-contributions/git-migration** 2 | 3 | I no longer maintain this project. 4 | 5 | --- 6 | 7 | 8 | # MCZ -> Git Migration 9 | [![Build Status](https://travis-ci.org/peteruhnak/git-migration.svg?branch=master)](https://travis-ci.org/peteruhnak/git-migration) [![Coverage Status](https://coveralls.io/repos/github/peteruhnak/git-migration/badge.svg?branch=master)](https://coveralls.io/github/peteruhnak/git-migration?branch=master) 10 | 11 | 12 | Utility to migrate code from SmalltalkHub (or any MCZ-based repo) to Git. 13 | 14 | The output is in **Tonel** format. 15 | 16 | ## Installation 17 | 18 | **Pharo 7** 19 | 20 | ```smalltalk 21 | Metacello new 22 | baseline: 'GitMigration'; 23 | repository: 'github://peteruhnak/git-migration/repository'; 24 | load 25 | ``` 26 | 27 | Pharo 6 is **not** supported. 28 | 29 | Table Of Contents 30 | 31 | * [Installation](#installation) 32 | * [Possible Issues](#possible-issues) 33 | * [Usage - Quick Example](#usage---quick-example) 34 | * [Usage - Detailed Example](#usage---detailed-example) 35 | * [Extras](#extras) 36 | * [Visualizations](#visualizations) 37 | * see pretty pictures of your MCZ history before you decide to migrate 38 | * [For Developers](#for-developers) 39 | * digging in the internals 40 | 41 | 42 | ## Possible Issues 43 | 44 | This tool has been used in countless successful migrations, however it is possible that you will run into a very special edge case™. Feel free to open an issue, contact me directly, on Pharo's mailing list or Discord. 45 | 46 | * **Corrupted MCZs:** Sometimes the MCZ that is on SmalltalkHub is corrupted. Although the MCZ contains a "backup" in form of a fileout, Pharo cannot actually correctly read this most of the time. The recommended solution is to just add the MCZ name to `#ignoredFileNames:`. 47 | * ![corrupted version](figures/corrupted-version.png) 48 | * **Private emails on GitHub:** If you use private email on GitHub, you will need to provide your GitHub-generated alias, otherwise the push will be rejected. This applies only to the email of the person pushing to GitHub, not all committers. 49 | 50 | * performance 51 | * downloading MCZs -- GitMigration is downloading *all* of your project MCZs from SmalltalkHub. This can take a while depending on the quality of your connection and how SmalltalkHub feels on any particular day 52 | * converting (in Pharo) -- each MCZ is read from disk, parsed, and written back to disk in a different format; this can take a while for large projects 53 | * e.g. PolyMath with 800 commits across 70 packages took ~3 minutes on a stock HDD 54 | * importing (Git) -- this should be under a minute; in most cases it will probably take couple of seconds 55 | 56 | * MCVersion dependencies are not supported (but I don't think they were used outside of Slices) 57 | * if you don't know what this is, you probably don't need to care 58 | * preserving proper merge history (see also [#4](https://github.com/peteruhnak/git-migration/issues/4)) 59 | * after many hours burned on this I've concluded that there is no way to do a fully automated 1:1 migration; note that your data/commits are not lost, only the merge history will not be as rich. 60 | 61 | ## Prerequisites 62 | 63 | * git installed in the system and available in `PATH` 64 | * **Pharo 7** 65 | 66 | ## Usage - Quick Example 67 | 68 | This tool generates a file for [git-fast-import](https://git-scm.com/docs/git-fast-import). 69 | 70 | 71 | ### 1. Add Source Repository 72 | 73 | Add your source repository (SmalltalkHub) to Pharo, e.g. via Monticello Browser 74 | 75 | ### 2. Find The Initial Commit SHA 76 | 77 | The migration will need to know from which commit it should start. This will be typically the SHA of the current commit of the master branch; you don't need the full 40-char SHA, an unambiguous prefix is enough. 78 | 79 | The get the current commit, you can use the following: 80 | 81 | ```bash 82 | $ git log --oneline -n 1 83 | ``` 84 | ### 3. Run Migration in Pharo 85 | 86 | See further down for a detailed line-by-line explanation. 87 | 88 | ```smalltalk 89 | "Pharo" 90 | migration := GitMigration on: 'peteruhnak/breaking-mcz'. 91 | " 92 | migration selectedPackageNames: #('Somewhere'). 93 | migration ignoredFileNames: #('Somewhere-PeterUhnak.2'). 94 | " 95 | migration onEmptyMessage: [ :info | 'empty commit message' ]. 96 | migration downloadAllVersions. 97 | migration populateCaches. 98 | migration allAuthors. 99 | migration authors: {'PeterUhnak' -> #('Peter Uhnak' '')}. 100 | migration 101 | fastImportCodeToDirectory: 'repository' 102 | initialCommit: '5793e82' 103 | to: 'D:/tmp/breaking-mcz2/import.txt' 104 | ``` 105 | 106 | ### 4. Import Code into Git 107 | 108 | ```bash 109 | # Terminal 110 | cd D:/tmp/breaking-mcz2 111 | git fast-import < import.txt 112 | git reset --hard master 113 | git gc 114 | ``` 115 | 116 | ## Usage - Detailed Example 117 | 118 | *A longer description of the example above.* 119 | 120 | ```smalltalk 121 | "Specify the name of the source repository; I am sourcing from peteruhnak/breaking-mcz project on SmalltalkHub" 122 | migration := GitMigration on: 'peteruhnak/breaking-mcz'. 123 | 124 | "optional -- migrate only some packages; if you don't specify anything, then all packages will be migrated" 125 | migration selectedPackageNames: #('Somewhere'). 126 | 127 | "optional -- in case you have corrupted MCZs, you can ignore them and rerun the migration" 128 | migration ignoredFileNames: #('Somewhere-PeterUhnak.2'). 129 | 130 | "if a MCZ was missing a commit message, you can provide an alternative; info is an instance of the problematic MCVersionInfo" 131 | migration onEmptyMessage: [ :info | 'empty commit message' ]. 132 | 133 | "Download all mcz files, this will take a while" 134 | migration downloadAllVersions. 135 | 136 | "Preload version metadata into the image." 137 | migration populateCaches. 138 | 139 | "List all authors anywhere in the project's commits" 140 | migration allAuthors. "#('PeterUhnak')" 141 | 142 | "You must specify name and email for _every_ author" 143 | "You must also specify the name/email for yourself (Author fullName), even if you haven't authored any code -- git treats separately the author of a commit and the commiter of a commit" 144 | 145 | "AuthorName (as shown in #allAuthors) -> #('Nicer Name' '')" 146 | migration authors: { 147 | 'PeterUhnak' -> #('Peter Uhnak' '') 148 | }. 149 | 150 | "Run the migration, this might take a while 151 | * the code directory is where the code will be stored (common practice is to have the code in `repository` subfolder, just like this project) 152 | * initialCommit is the commit from which the migration should start 153 | * to is where the git-fast-import file should be stored" 154 | migration 155 | fastImportCodeToDirectory: 'repository' 156 | initialCommit: '5793e82' 157 | to: 'D:/tmp/breaking-mcz2/import.txt' 158 | ``` 159 | 160 | ### Running The Import 161 | 162 | Get a terminal, go to the target git repository, and run the migration. 163 | 164 | ```bash 165 | # import.txt is the file that you've created earlier 166 | $ git fast-import < import.txt 167 | # fast-import doesn't change the working directory, so we need to update it 168 | $ git reset --hard master 169 | # (optional) garbage collection: fast import leaves a lot of mess behind 170 | # happens automatically on commit since Git >=2.17 171 | $ git gc 172 | ``` 173 | 174 | You should see the changes, and `git log` should show you the entire history. 175 | 176 | ## Git Tips 177 | 178 | Forgetting all changes in the history and going back to previous state. Useful if the migration is botched and you want to rollback all changes. 179 | 180 | ```bash 181 | $ git reset --hard SHA 182 | ``` 183 | 184 | ## Extras 185 | 186 | If you want to play around with the version data before committing, read the following. 187 | 188 | ```smalltalk 189 | migration := GitMigration on: 'peteruhnak/breaking-mcz'. 190 | ``` 191 | 192 | Downloading all MCZs from server; this needs to happen only once and can take couple of minutes for large repos. 193 | 194 | ```smalltalk 195 | migration cacheAllVersions. 196 | ``` 197 | 198 | List all packages in the repository that have multiple roots; although rare, this could be either result of multiple people starting independently on the same package, or a mistake was made during committing. 199 | GitMigration should be able to handle this correctly regardless. 200 | ```smalltalk 201 | migration packagesWithMultipleRoots. 202 | ``` 203 | 204 | List all authors in the repository. 205 | ```smalltalk 206 | migration allAuthors. 207 | ``` 208 | 209 | Dictionary of all packages and their _real_ (see later what's real) commits. 210 | ```smalltalk 211 | versionsByPackage := migration versionsByPackage. 212 | ``` 213 | 214 | *All* versions of a package, whether there is actually an MCZ or not. With Monticello it is very easy to create a commit whose ancestor is not in the repository, so it is not obvious how the commit connects the previous ones. 215 | Thankfully MCZ typically contains the hierarchy many steps back, so we can correctly reconstruct the whole tree. 216 | ```smalltalk 217 | allVersions := migration completeAncestryOfPackageNamed: 'Somewhere'. 218 | ``` 219 | 220 | The versions in mcz are random, so we need to sort them in an order in which we can commit them to git. This means that all ancestry is honored (no child is commited before its parent), and "sibling" commits are sorted by date. 221 | Note that we cannot just sort the commits by date, because the date might not follow the ancestry correctly (which can happen, especially if different timezones are involved, which MC doesn't keep track of) 222 | ```smalltalk 223 | sorted := migration topologicallySort: allVersions. 224 | ``` 225 | 226 | Get the total ordering of all commits across all packages 227 | ```smalltalk 228 | allVersionsOrdered := migration commitOrder. 229 | ``` 230 | 231 | ## Visualizations 232 | 233 | This requires [Roassal](http://agilevisualization.com/) to be installed (available in catalog). 234 | 235 | In all visualizations hovering over an item will show a popup with more information, and clicking on item will open an inspector. 236 | Keep in mind that running the command will not open a new window, so you have to either inspect it, or do-it-and-go in playground. 237 | 238 | ### Single Package Ancestry 239 | 240 | Looking at raw data is not very insightful, so couple visualization are included. 241 | 242 | ```smalltalk 243 | migration := GitMigration on: 'peteruhnak/breaking-mcz'. 244 | migration cacheAllVersions. 245 | visualization := migration visualization. 246 | ``` 247 | 248 | Show the complete ancestry of a single package. 249 | ```smalltalk 250 | visualization showAncestryTopologyOnPackageNamed: 'Somewhere'. 251 | ``` 252 | 253 | ![](figures/package-ancestry.png) 254 | 255 | * Yellow - root versions (versions with no parents, typically only a single initial commit) 256 | * Cyan - tail/head versions (versions with no children, typically the latest version(s)) 257 | * Magenta - "virtual" versions that do not have a corresponding commit (this happens as mentioned earlier) 258 | 259 | The number on the third line indicates in what order the packages will be committed (magenta packages are listed, but are not committed, because there is no code to commit). 260 | Keep in mind that the number in the commit (Somewhere-PeterUhnak.15) has no meaning, and can be easily changed (and broken by hand) when committing. 261 | 262 | 263 | ### Project Ancestry 264 | 265 | To see all packages and history, you could do. 266 | 267 | ```smalltalk 268 | visualization showProjectAncestry. 269 | ``` 270 | 271 | ![](figures/project-ancestry.png) 272 | 273 | This is useful if you want to quickly glance at a project (and is also much faster to generate and use), but if want you can also add label 274 | 275 | ```smalltalk 276 | visualization showProjectAncestryWithLabels. 277 | "or" 278 | visualization showProjectAncestryWithLabels: true. 279 | ``` 280 | 281 | ![](figures/project-ancestry-labels.png) 282 | 283 | 284 | ### Limited Project Ancestry 285 | 286 | If you have big project and want to look only at certain packages, you can do so. (In the image you can see that the longest chain has ancestry broken - red box at the end) 287 | 288 | ``` 289 | migration := GitMigration on: 'PolyMath/PolyMath'. 290 | migration cacheAllVersions. 291 | visualization := migration visualization. 292 | "or just a collection of package names" 293 | visualization showProjectAncestryOn: (allPackages copyWithoutAll: #('Monticello' 'ConfigurationOfSciSmalltalk' 'Math-RealInterval')). 294 | ``` 295 | 296 | ![](figures/subset-packages-ancestry.png) 297 | 298 | Adding labels works the same way 299 | 300 | ``` 301 | visualization showProjectAncestryOn: aCollectionOfPackages withLabels: aBoolean 302 | ``` 303 | 304 | ## For Developers 305 | 306 | Some hints and random thoughts. 307 | SmalltalkHub stores every commit in a separate MCZ file, which contains some metadata about the commit (name, ancestry, etc), as well as all the code. The code itself is not incremental, rather code in each zip is as-is. 308 | 309 | This means that when GitFileTree is exporting, it will remove all files on the disk, unpack the MCZ file, and write all the code back to disk, and commit. Git is smart enough to only commit what has actually changed, however for GFT this operation is very IO intense - if you have 5k files in your code base and you changed just a single method (which is common), then 5k files will be removed and then added back... you can imagine what this does to the disk when performed 1000x times (once for each commit). 310 | 311 | With fast-import I've made a workaround for this. A pseudo-repository `GitMigrationMemoryTreeGitRepository` is created that uses memory file system as the target directory. This way the fileout doesn't write to real disk and everything is kept in RAM, which improves the performance significantly. 312 | 313 | Note however that instead of using `MemoryStore` I had to subclass it (`GitMigrationMemoryStore`) to properly handle path separators; on Windows, MemoryStore by itself will create files and directories with slashes (both forward and backward) in their names instead of creating a hierarchy, so my `GitMigrationMemoryStore` fixes this. 314 | 315 | I am also subclassing `MemoryHandle` (`GitMigrationMemoryHandle`) and I've changed the `writeStream` of it to return `MultiByteBinaryOrTextStream`. This is because `MemoryStore` returns only an ordinary `WriteStream` which cannot handle unicode content and 那不是很好。 :) 316 | -------------------------------------------------------------------------------- /figures/corrupted-version.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peteruhnak/git-migration/37b80ebfe6bb0a95556f2c03ab24e34727b0c5ee/figures/corrupted-version.png -------------------------------------------------------------------------------- /figures/package-ancestry.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peteruhnak/git-migration/37b80ebfe6bb0a95556f2c03ab24e34727b0c5ee/figures/package-ancestry.png -------------------------------------------------------------------------------- /figures/project-ancestry-labels.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peteruhnak/git-migration/37b80ebfe6bb0a95556f2c03ab24e34727b0c5ee/figures/project-ancestry-labels.png -------------------------------------------------------------------------------- /figures/project-ancestry.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peteruhnak/git-migration/37b80ebfe6bb0a95556f2c03ab24e34727b0c5ee/figures/project-ancestry.png -------------------------------------------------------------------------------- /figures/subset-packages-ancestry.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peteruhnak/git-migration/37b80ebfe6bb0a95556f2c03ab24e34727b0c5ee/figures/subset-packages-ancestry.png -------------------------------------------------------------------------------- /figures/testing-source.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peteruhnak/git-migration/37b80ebfe6bb0a95556f2c03ab24e34727b0c5ee/figures/testing-source.png -------------------------------------------------------------------------------- /repository/.properties: -------------------------------------------------------------------------------- 1 | { 2 | #format : #tonel 3 | } 4 | } -------------------------------------------------------------------------------- /repository/BaselineOfGitMigration/BaselineOfGitMigration.class.st: -------------------------------------------------------------------------------- 1 | " 2 | Baseline for https://github.com/peteruhnak/git-migration 3 | " 4 | Class { 5 | #name : #BaselineOfGitMigration, 6 | #superclass : #BaselineOf, 7 | #category : 'BaselineOfGitMigration' 8 | } 9 | 10 | { #category : #baselines } 11 | BaselineOfGitMigration >> baseline: spec [ 12 | 13 | spec 14 | for: #common 15 | do: [ spec 16 | baseline: 'GitFastWriter' 17 | with: [ spec repository: 'github://peteruhnak/git-fast-writer/repository' ]. 18 | spec package: 'GitMigration' with: [ spec requires: 'GitFastWriter' ]. 19 | spec group: 'default' with: #('GitMigration') ] 20 | ] 21 | -------------------------------------------------------------------------------- /repository/BaselineOfGitMigration/package.st: -------------------------------------------------------------------------------- 1 | Package { #name : #BaselineOfGitMigration } 2 | -------------------------------------------------------------------------------- /repository/GitMigration/GitMigration.class.st: -------------------------------------------------------------------------------- 1 | " 2 | I am the main user entrypoint for performing migration. 3 | 4 | See class-side for an example, or read the docs ( https://github.com/peteruhnak/git-migration ) 5 | " 6 | Class { 7 | #name : #GitMigration, 8 | #superclass : #Object, 9 | #instVars : [ 10 | 'authors', 11 | 'repository', 12 | 'cachedVersions', 13 | 'completeAncestryCache', 14 | 'versionsWithPackageNames', 15 | 'versionsCache', 16 | 'allAuthors', 17 | 'selectedPackageNames', 18 | 'onEmptyMessage', 19 | 'ignoredFileNames' 20 | ], 21 | #category : #GitMigration 22 | } 23 | 24 | { #category : #'instance creation' } 25 | GitMigration class >> on: aProjectName [ 26 | ^ self new 27 | projectName: aProjectName; 28 | yourself 29 | ] 30 | 31 | { #category : #accessing } 32 | GitMigration >> allAuthors [ 33 | allAuthors 34 | ifNil: [ | authorsSet | 35 | authorsSet := Set new. 36 | authorsSet add: Author fullName. 37 | self versions 38 | do: [ :each | 39 | authorsSet add: each info author. 40 | authorsSet addAll: (self authorsInSnapshot: each veryDeepCopy snapshot) ] 41 | displayingProgress: [ :each | 'Loading authors... ' , each info name ]. 42 | allAuthors := authorsSet asArray sorted ]. 43 | ^ allAuthors 44 | ] 45 | 46 | { #category : #accessing } 47 | GitMigration >> authors: aCollection [ 48 | authors := aCollection asDictionary 49 | ] 50 | 51 | { #category : #accessing } 52 | GitMigration >> authorsInSnapshot: aSnapshot [ 53 | | timeStamps | 54 | timeStamps := OrderedCollection new. 55 | aSnapshot definitions 56 | do: [ :each | 57 | each isMethodDefinition 58 | ifTrue: [ timeStamps add: each timeStamp ]. 59 | (each isClassDefinition and: [ (each isKindOf: MCClassTraitDefinition) not ]) 60 | ifTrue: [ timeStamps add: each commentStamp ] ]. 61 | ^ (timeStamps collect: [ :each | each copyUpTo: Character space ]) \ #('' '') 62 | ] 63 | 64 | { #category : #actions } 65 | GitMigration >> cacheAllVersions [ 66 | | goSource allPackageNames packageNames| 67 | goSource := Gofer new repository: repository. 68 | allPackageNames := (repository allVersionNames collect: [ :each | each copyUpToLast: $- ]) asSet. 69 | packageNames := selectedPackageNames ifEmpty: [ allPackageNames ]. 70 | (goSource allResolved 71 | select: [ :resolved | packageNames includes: resolved packageName ]) 72 | do: [ :each | goSource package: each packageName ]. 73 | goSource fetch 74 | ] 75 | 76 | { #category : #actions } 77 | GitMigration >> commitOrder [ 78 | "All packages to be commited in the order from first to last, across multiple packages" 79 | 80 | | packagePool ordering | 81 | packagePool := IdentitySet new. 82 | ordering := OrderedCollection new. 83 | self versionsByPackage 84 | keysAndValuesDo: [ :pkgName :realVersions | 85 | | allVersions allRealVersions allValidVersions | 86 | allVersions := self topologicallySort: (self completeAncestryOfPackageNamed: pkgName). 87 | "add only versions that have real MCZ file" 88 | allRealVersions := allVersions select: [ :each | realVersions includes: each ]. 89 | "reject versions with empty snapshot; veryDeepCopy because the version is kept in memory, but not the snapshot" 90 | allValidVersions := allRealVersions 91 | reject: [ :each | 92 | | fullVersion | 93 | fullVersion := self versions detect: [ :v | v info = each ]. 94 | fullVersion veryDeepCopy snapshot definitions isEmpty ]. 95 | " self haltIf: [ (allValidVersions = allRealVersions) not ]." 96 | allValidVersions 97 | ifEmpty: [ self notify: 'Package ' , pkgName , ' has no MCZs with code' ]. 98 | packagePool add: allValidVersions ]. 99 | packagePool := packagePool reject: #isEmpty. 100 | 101 | "pick the oldest available commit across all packages" 102 | [ packagePool isNotEmpty ] 103 | whileTrue: [ | oldestPackage version | 104 | oldestPackage := packagePool detectMin: [ :pkgVersions | pkgVersions first timeStamp ]. 105 | version := oldestPackage first. 106 | oldestPackage removeFirst. 107 | ordering add: version. 108 | oldestPackage ifEmpty: [ packagePool remove: oldestPackage ] ]. 109 | ^ ordering 110 | ] 111 | 112 | { #category : #accessing } 113 | GitMigration >> completeAncestryOfPackageNamed: aPackageName [ 114 | ^ completeAncestryCache 115 | at: aPackageName 116 | ifAbsentPut: [ | versions allVersions getAncestors parents | 117 | "Not all versions are actually available directly, so do a very deep search" 118 | versions := self versionsByPackage at: aPackageName. 119 | allVersions := Set new. 120 | getAncestors := [ :parent | 121 | (allVersions includes: parent) 122 | ifTrue: [ #() ] 123 | ifFalse: [ parent ancestors ] ]. 124 | versions 125 | do: [ :version | 126 | parents := Array with: version. 127 | [ parents isNotEmpty ] 128 | whileTrue: [ | allAncestors ancestors | 129 | allAncestors := parents flatCollect: [ :p | getAncestors value: p ]. 130 | "The history can get trimmed, with only ID preserved and nothing else" 131 | ancestors := allAncestors reject: [ :e | e name = '' ]. 132 | allVersions addAll: parents. 133 | parents := ancestors ] ]. 134 | allVersions ] 135 | ] 136 | 137 | { #category : #import } 138 | GitMigration >> createFastImportAt: anInitialCommitish usingWriter: aWriter [ 139 | | versions | 140 | aWriter initialCommit: anInitialCommitish. 141 | self useAuthorsOn: aWriter. 142 | self do: [ versions := self versionsToMigrate ] displaying: 'Ordering history...'. 143 | self preChecksOn: versions. 144 | versions 145 | do: [ :each | aWriter writeVersion: each veryDeepCopy ] 146 | displayingProgress: [ :each | 'Exporting version ' , each info name ] 147 | ] 148 | 149 | { #category : #ui } 150 | GitMigration >> do: aBlock displaying: aString [ 151 | aString 152 | displayProgressFrom: 0 153 | to: 2 154 | during: [ :bar | 155 | bar value: 1. 156 | World doOneCycle. 157 | aBlock value. 158 | bar value: 2 ] 159 | ] 160 | 161 | { #category : #actions } 162 | GitMigration >> downloadAllVersions [ 163 | self do: [ self cacheAllVersions ] displaying: 'Downloading all versions...' 164 | ] 165 | 166 | { #category : #import } 167 | GitMigration >> fastImportCodeToDirectory: aDirectoryName initialCommit: anInitialCommitish to: aFileReference [ 168 | | writer | 169 | aFileReference asFileReference 170 | ensureDelete; 171 | writeStreamDo: [ :rawStream | 172 | writer := GitMigrationTonelWriter new on: rawStream. 173 | writer exportDirectory: aDirectoryName. 174 | writer onEmptyMessage: onEmptyMessage. 175 | self createFastImportAt: anInitialCommitish usingWriter: writer ]. 176 | ^ writer commitMarks 177 | ] 178 | 179 | { #category : #retrieving } 180 | GitMigration >> findRepositoryNamed: aRepoName [ 181 | "aRepoName = ownerName/projectName, e.g. ObjectProfile/Roassal2" 182 | ^ MCRepositoryGroup default repositories 183 | detect: [ :each | (each description includesSubstring: aRepoName) or: (each description includesSubstring: (aRepoName copyReplaceAll: '/' with: '\')) ] 184 | ] 185 | 186 | { #category : #accessing } 187 | GitMigration >> ignoredFileNames: aCollection [ 188 | ignoredFileNames := aCollection collect: [ :each | each withoutSuffix: '.mcz' ] 189 | ] 190 | 191 | { #category : #initialization } 192 | GitMigration >> initialize [ 193 | super initialize. 194 | completeAncestryCache := Dictionary new. 195 | selectedPackageNames := #(). 196 | ignoredFileNames := #(). 197 | onEmptyMessage := [ :info | self error: 'Empty message was requested' ] 198 | ] 199 | 200 | { #category : #'topology sorting' } 201 | GitMigration >> isRoot: aKey in: pairs [ 202 | ^ pairs noneSatisfy: [ :pair | pair value = aKey ] 203 | ] 204 | 205 | { #category : #accessing } 206 | GitMigration >> onEmptyMessage: aBlock [ 207 | onEmptyMessage := aBlock 208 | ] 209 | 210 | { #category : #accessing } 211 | GitMigration >> packagesWithMultipleRoots [ 212 | | multiRoots | 213 | multiRoots := Dictionary new. 214 | self versionsByPackage 215 | keysAndValuesDo: [ :pkgName :versions | 216 | | roots | 217 | roots := (self completeAncestryOfPackageNamed: pkgName) 218 | select: [ :each | each ancestors isEmpty ]. 219 | roots size > 1 220 | ifTrue: [ multiRoots at: pkgName put: roots ] ]. 221 | ^ multiRoots 222 | ] 223 | 224 | { #category : #actions } 225 | GitMigration >> populateCaches [ 226 | self 227 | do: [ self versionsWithPackageNames. 228 | self versions ] 229 | displaying: 'Preparing data...' 230 | ] 231 | 232 | { #category : #import } 233 | GitMigration >> preCheckOn: aVersion [ 234 | "NOTE: do not touch snapshot without performing veryDeepCopy first" 235 | 236 | aVersion info message 237 | ifEmpty: [ self 238 | assert: [ onEmptyMessage isNotNil and: [ (onEmptyMessage cull: aVersion info) isNotEmpty ] ] 239 | description: [ 'You must provide onEmptyMessage.' ] ] 240 | ] 241 | 242 | { #category : #import } 243 | GitMigration >> preChecksOn: aVersionList [ 244 | aVersionList do: [ :each | self preCheckOn: each ] 245 | ] 246 | 247 | { #category : #accessing } 248 | GitMigration >> projectName: aProjectName [ 249 | repository := self findRepositoryNamed: aProjectName 250 | ] 251 | 252 | { #category : #accessing } 253 | GitMigration >> repository [ 254 | ^ repository 255 | ] 256 | 257 | { #category : #accessing } 258 | GitMigration >> selectedPackageNames: aCollection [ 259 | selectedPackageNames := aCollection 260 | ] 261 | 262 | { #category : #'topology sorting' } 263 | GitMigration >> topologicallySort: anAncestry [ 264 | | ancestry | 265 | anAncestry size = 1 266 | ifTrue: [ ^ anAncestry asOrderedCollection ]. 267 | ancestry := anAncestry asArray 268 | flatCollect: [ :each | 269 | each ancestors collect: [ :anc | anc -> each ] ]. 270 | ^ self topologicallySortPairs: ancestry 271 | ] 272 | 273 | { #category : #'topology sorting' } 274 | GitMigration >> topologicallySortPairs: anArray [ 275 | | in out pairs | 276 | in := Set new. 277 | out := OrderedCollection new. 278 | pairs := anArray asOrderedCollection. 279 | in addAll: (pairs select: [ :pair | self isRoot: pair key in: pairs ] thenCollect: #key). 280 | [ in isNotEmpty ] 281 | whileTrue: [ | current next | 282 | current := in detectMin: #timeStamp. 283 | in remove: current. 284 | out add: current. 285 | next := pairs select: [ :pair | pair key = current ]. 286 | pairs removeAll: next. 287 | in addAll: (next collect: #value thenSelect: [ :each | self isRoot: each in: pairs ]) ]. 288 | ^ out 289 | ] 290 | 291 | { #category : #import } 292 | GitMigration >> useAuthorsOn: aWriter [ 293 | (authors isNil or: [ authors isEmpty ]) 294 | ifTrue: [ self error: 'Please provide authors.' ]. 295 | authors 296 | keysAndValuesDo: [ :key :duet | 297 | aWriter authorMapping 298 | shortName: key 299 | name: duet first 300 | email: ((duet second withoutPrefix: '<') withoutSuffix: '>') ] 301 | ] 302 | 303 | { #category : #retrieving } 304 | GitMigration >> versionFromFileNamed: aFileName [ 305 | ^ MCCacheRepository uniqueInstance versionReaderForFileNamed: aFileName do: #version 306 | ] 307 | 308 | { #category : #retrieving } 309 | GitMigration >> versionInfoFromFileNamed: aFileName [ 310 | ^ MCCacheRepository uniqueInstance versionInfoFromFileNamed: aFileName 311 | ] 312 | 313 | { #category : #accessing } 314 | GitMigration >> versions [ 315 | versionsCache 316 | ifNil: [ | versions | 317 | versions := OrderedCollection new. 318 | self versionsWithPackageNames 319 | do: [ :quad | versions add: (self versionFromFileNamed: quad last) ] 320 | displayingProgress: [ :quad | 'Loading versions metadata... ' , quad last ]. 321 | versionsCache := versions ]. 322 | ^ versionsCache 323 | ] 324 | 325 | { #category : #accessing } 326 | GitMigration >> versionsByPackage [ 327 | | versionsByPackage | 328 | versionsByPackage := Dictionary new. 329 | self versionsInfo 330 | do: [ :version | 331 | (versionsByPackage 332 | at: (version name copyUpToLast: $-) 333 | ifAbsentPut: [ OrderedCollection new ]) add: version ]. 334 | ^ versionsByPackage 335 | ] 336 | 337 | { #category : #accessing } 338 | GitMigration >> versionsInfo [ 339 | ^ self versions collect: #info 340 | ] 341 | 342 | { #category : #import } 343 | GitMigration >> versionsToMigrate [ 344 | | totalOrdering versions | 345 | totalOrdering := self commitOrder. 346 | versions := totalOrdering 347 | collect: [ :info | self versions detect: [ :version | version info = info ] ]. 348 | versions removeAllSuchThat: [ :each | ignoredFileNames includes: each info name ]. 349 | ^ versions 350 | ] 351 | 352 | { #category : #accessing } 353 | GitMigration >> versionsWithPackageNames [ 354 | ^ versionsWithPackageNames 355 | ifNil: [ | all selected | 356 | all := repository versionsWithPackageNames. 357 | versionsWithPackageNames := selectedPackageNames 358 | ifEmpty: [ all ] 359 | ifNotEmpty: [ all select: [ :each | selectedPackageNames includes: each first ] ] ] 360 | ] 361 | 362 | { #category : #visualizations } 363 | GitMigration >> visualization [ 364 | ^ GitMigrationVisualization new migration: self 365 | ] 366 | -------------------------------------------------------------------------------- /repository/GitMigration/GitMigrationAuthor.class.st: -------------------------------------------------------------------------------- 1 | " 2 | I represent a git commit author (email + name). 3 | 4 | shortName is an identifier to match the author against a MC commit author. 5 | " 6 | Class { 7 | #name : #GitMigrationAuthor, 8 | #superclass : #Object, 9 | #instVars : [ 10 | 'shortName', 11 | 'name', 12 | 'email' 13 | ], 14 | #category : #'GitMigration-FastImport' 15 | } 16 | 17 | { #category : #'instance creation' } 18 | GitMigrationAuthor class >> shortName: aShortName name: aName email: anEmail [ 19 | ^ self new 20 | shortName: aShortName; 21 | name: aName; 22 | email: anEmail; 23 | yourself 24 | ] 25 | 26 | { #category : #accessing } 27 | GitMigrationAuthor >> email [ 28 | ^ email 29 | ] 30 | 31 | { #category : #accessing } 32 | GitMigrationAuthor >> email: aString [ 33 | email := aString 34 | ] 35 | 36 | { #category : #accessing } 37 | GitMigrationAuthor >> name [ 38 | ^ name 39 | ] 40 | 41 | { #category : #accessing } 42 | GitMigrationAuthor >> name: anObject [ 43 | name := anObject 44 | ] 45 | 46 | { #category : #accessing } 47 | GitMigrationAuthor >> shortName [ 48 | ^ shortName 49 | ] 50 | 51 | { #category : #accessing } 52 | GitMigrationAuthor >> shortName: aString [ 53 | shortName := aString 54 | ] 55 | -------------------------------------------------------------------------------- /repository/GitMigration/GitMigrationAuthorMapping.class.st: -------------------------------------------------------------------------------- 1 | " 2 | I am a collection of author identifiers. 3 | " 4 | Class { 5 | #name : #GitMigrationAuthorMapping, 6 | #superclass : #Object, 7 | #instVars : [ 8 | 'mapping' 9 | ], 10 | #category : 'GitMigration-FastImport' 11 | } 12 | 13 | { #category : #'instance creation' } 14 | GitMigrationAuthorMapping >> at: aShortName [ 15 | ^ mapping at: aShortName 16 | ] 17 | 18 | { #category : #'instance creation' } 19 | GitMigrationAuthorMapping >> initialize [ 20 | super initialize. 21 | mapping := Dictionary new 22 | ] 23 | 24 | { #category : #'instance creation' } 25 | GitMigrationAuthorMapping >> shortName: aShortName name: aFullName email: anEmail [ 26 | mapping 27 | at: aShortName 28 | put: (GitMigrationAuthor shortName: aShortName name: aFullName email: anEmail) 29 | ] 30 | -------------------------------------------------------------------------------- /repository/GitMigration/GitMigrationAuthorMappingTest.class.st: -------------------------------------------------------------------------------- 1 | Class { 2 | #name : #GitMigrationAuthorMappingTest, 3 | #superclass : #TestCase, 4 | #category : 'GitMigration-Tests' 5 | } 6 | 7 | { #category : #tests } 8 | GitMigrationAuthorMappingTest >> testRetrieve [ 9 | | mapping author | 10 | mapping := GitMigrationAuthorMapping new. 11 | mapping shortName: 'ImportBot' name: 'Import Bot' email: 'importbot@example.com'. 12 | author := mapping at: 'ImportBot'. 13 | self assert: author shortName equals: 'ImportBot'. 14 | self assert: author name equals: 'Import Bot'. 15 | self assert: author email equals: 'importbot@example.com' 16 | ] 17 | -------------------------------------------------------------------------------- /repository/GitMigration/GitMigrationCommitInfo.class.st: -------------------------------------------------------------------------------- 1 | " 2 | I represent a single commit in the history. 3 | " 4 | Class { 5 | #name : #GitMigrationCommitInfo, 6 | #superclass : #Object, 7 | #instVars : [ 8 | 'info', 9 | 'author', 10 | 'committer', 11 | 'parents', 12 | 'branch', 13 | 'coauthors', 14 | 'onEmptyMessage' 15 | ], 16 | #category : #'GitMigration-FastImport' 17 | } 18 | 19 | { #category : #'instance creation' } 20 | GitMigrationCommitInfo class >> info: aVersionInfo mapping: aMapping committer: aCommitter coauthors: aCoauthorsList onEmptyMessage: aBlock [ 21 | ^ self new 22 | info: aVersionInfo; 23 | author: (aMapping at: aVersionInfo author); 24 | coauthors: (aCoauthorsList \ {aVersionInfo author} collect: [ :each | aMapping at: each ]); 25 | committer: aCommitter; 26 | onEmptyMessage: aBlock 27 | ] 28 | 29 | { #category : #accessing } 30 | GitMigrationCommitInfo >> author [ 31 | ^ author 32 | ] 33 | 34 | { #category : #accessing } 35 | GitMigrationCommitInfo >> author: anAuthor [ 36 | author := anAuthor 37 | ] 38 | 39 | { #category : #accessing } 40 | GitMigrationCommitInfo >> authorEmail [ 41 | ^ author email 42 | ] 43 | 44 | { #category : #accessing } 45 | GitMigrationCommitInfo >> authorName [ 46 | ^ author name 47 | ] 48 | 49 | { #category : #accessing } 50 | GitMigrationCommitInfo >> authoredDate [ 51 | "MCVersionInfo has no information of the timezone, so Pharo automatically adds the local TZ of the user; as this depends where the user is, we translate it to UTC, so two people in different timezones see the same time" 52 | 53 | ^ info timeStamp translateToUTC rounded 54 | ] 55 | 56 | { #category : #accessing } 57 | GitMigrationCommitInfo >> branch [ 58 | ^ branch 59 | ] 60 | 61 | { #category : #accessing } 62 | GitMigrationCommitInfo >> branch: anObject [ 63 | branch := anObject 64 | ] 65 | 66 | { #category : #accessing } 67 | GitMigrationCommitInfo >> coauthors [ 68 | ^ coauthors \ {author} 69 | ] 70 | 71 | { #category : #accessing } 72 | GitMigrationCommitInfo >> coauthors: aCollection [ 73 | coauthors := aCollection 74 | ] 75 | 76 | { #category : #accessing } 77 | GitMigrationCommitInfo >> commitMessage [ 78 | ^ String 79 | << [ :stream | 80 | | message | 81 | message := (info message copyWithout: Character null) withUnixLineEndings. 82 | message 83 | ifEmpty: 84 | [ message := ((onEmptyMessage cull: info) copyWithout: Character null) withUnixLineEndings ]. 85 | self assert: [ message isNotEmpty ] description: [ 'Commit message must not be empty' ]. 86 | stream << message. 87 | (self coauthors sorted: #name ascending) 88 | ifNotEmpty: [ :co | 89 | stream lf. 90 | stream lf. 91 | co 92 | do: [ :each | stream << 'Co-authored-by: ' << each name << ' <' << each email << '>' ] 93 | separatedBy: [ stream lf ] ] ] 94 | ] 95 | 96 | { #category : #accessing } 97 | GitMigrationCommitInfo >> committedDate [ 98 | ^ DateAndTime now 99 | ] 100 | 101 | { #category : #accessing } 102 | GitMigrationCommitInfo >> committer [ 103 | ^ committer 104 | ] 105 | 106 | { #category : #accessing } 107 | GitMigrationCommitInfo >> committer: anAuthor [ 108 | committer := anAuthor 109 | ] 110 | 111 | { #category : #accessing } 112 | GitMigrationCommitInfo >> committerEmail [ 113 | ^ committer email 114 | ] 115 | 116 | { #category : #accessing } 117 | GitMigrationCommitInfo >> committerName [ 118 | ^ committer name 119 | ] 120 | 121 | { #category : #accessing } 122 | GitMigrationCommitInfo >> info [ 123 | ^ info 124 | ] 125 | 126 | { #category : #accessing } 127 | GitMigrationCommitInfo >> info: anObject [ 128 | info := anObject 129 | ] 130 | 131 | { #category : #accessing } 132 | GitMigrationCommitInfo >> initialize [ 133 | super initialize. 134 | branch := 'master' 135 | ] 136 | 137 | { #category : #accessing } 138 | GitMigrationCommitInfo >> onEmptyMessage: aBlock [ 139 | onEmptyMessage := aBlock 140 | ] 141 | 142 | { #category : #accessing } 143 | GitMigrationCommitInfo >> parents [ 144 | ^ parents 145 | ] 146 | 147 | { #category : #accessing } 148 | GitMigrationCommitInfo >> parents: anObject [ 149 | parents := anObject 150 | ] 151 | 152 | { #category : #printing } 153 | GitMigrationCommitInfo >> printOn: aStream [ 154 | aStream 155 | << self info versionNumber; 156 | << ' ['. 157 | self parents 158 | do: [ :each | aStream << each info versionNumber ] 159 | separatedBy: [ aStream << ', ' ]. 160 | aStream << ']'. 161 | aStream 162 | << ' @'; 163 | << self branch name 164 | ] 165 | -------------------------------------------------------------------------------- /repository/GitMigration/GitMigrationCommitInfoTest.class.st: -------------------------------------------------------------------------------- 1 | Class { 2 | #name : #GitMigrationCommitInfoTest, 3 | #superclass : #TestCase, 4 | #instVars : [ 5 | 'authorMapping', 6 | 'committer', 7 | 'writer' 8 | ], 9 | #category : 'GitMigration-Tests' 10 | } 11 | 12 | { #category : #'instance creation' } 13 | GitMigrationCommitInfoTest >> commitInfoFor: aVersionInfo [ 14 | ^ writer commitInfoFor: aVersionInfo 15 | ] 16 | 17 | { #category : #running } 18 | GitMigrationCommitInfoTest >> ensureAllCached [ 19 | (MCCacheRepository uniqueInstance directory children 20 | noneSatisfy: [ :each | each basename = 'Somewhere-PeterUhnak.2.mcz' ]) 21 | ifTrue: [ (GitMigration on: 'peteruhnak/breaking-mcz') cacheAllVersions ] 22 | ] 23 | 24 | { #category : #running } 25 | GitMigrationCommitInfoTest >> ensureGitFileTreeLoaded [ 26 | (Smalltalk hasClassNamed: #MCFileTreeGitRepository) 27 | ifFalse: [ (CatalogProvider projectNamed: 'GitFileTree') installVersion: #stable ] 28 | ] 29 | 30 | { #category : #running } 31 | GitMigrationCommitInfoTest >> ensureTestRepoLoaded [ 32 | | repo | 33 | repo := MCSmalltalkhubRepository new. 34 | repo owner: 'peteruhnak'. 35 | repo project: 'breaking-mcz'. 36 | MCRepositoryGroup default addRepository: repo 37 | ] 38 | 39 | { #category : #running } 40 | GitMigrationCommitInfoTest >> setUp [ 41 | super setUp. 42 | self timeLimit: 1 minute. 43 | self ensureTestRepoLoaded. 44 | " self ensureGitFileTreeLoaded." 45 | self ensureAllCached. 46 | writer := GitMigrationFastImportWriter new. 47 | authorMapping := GitMigrationAuthorMapping new 48 | shortName: 'ImportBot' name: 'Import Bot' email: 'importbot@example.com'; 49 | shortName: 'CommitterBot' name: 'Committer Bot' email: 'committerbot@example.com'; 50 | shortName: 'JoDoe' name: 'Jo Doe' email: ''; 51 | shortName: 'SamDoe' name: 'Sam Doe' email: 'samdoe@example.com'. 52 | committer := authorMapping at: 'CommitterBot'. 53 | writer committerName: 'CommitterBot'. 54 | writer authorMapping: authorMapping 55 | ] 56 | 57 | { #category : #'tests - commit transform' } 58 | GitMigrationCommitInfoTest >> testAuthorEmail [ 59 | self 60 | assert: (self commitInfoFor: self versionWithoutParent) authorEmail 61 | equals: 'importbot@example.com' 62 | ] 63 | 64 | { #category : #'tests - commit transform' } 65 | GitMigrationCommitInfoTest >> testAuthorName [ 66 | self 67 | assert: (self commitInfoFor: self versionWithoutParent) authorName 68 | equals: 'Import Bot' 69 | ] 70 | 71 | { #category : #'tests - commit transform' } 72 | GitMigrationCommitInfoTest >> testAuthoredDate [ 73 | self 74 | assert: (self commitInfoFor: self versionWithoutParent) authoredDate 75 | equals: (DateAndTime fromUnixTime: 977329230) "'2000-12-20T16:20:30+00:00'" 76 | ] 77 | 78 | { #category : #'tests - commit transform' } 79 | GitMigrationCommitInfoTest >> testCommitMessage [ 80 | self 81 | assert: (self commitInfoFor: self versionWithoutParent) commitMessage 82 | equals: 'Initial MC commit' 83 | ] 84 | 85 | { #category : #'tests - commit transform' } 86 | GitMigrationCommitInfoTest >> testCommitMessageNull [ 87 | "https://github.com/peteruhnak/git-migration/issues/15" 88 | 89 | self 90 | assert: (self commitInfoFor: self versionWithNull) commitMessage 91 | equals: 'Null here >< and there ><' withUnixLineEndings 92 | ] 93 | 94 | { #category : #'tests - commit transform' } 95 | GitMigrationCommitInfoTest >> testCommitMessageWithCoauthors [ 96 | self 97 | assert: (self commitInfoFor: self versionWithCoauthors) commitMessage 98 | equals: 99 | 'Version with coauthors 100 | 101 | Co-authored-by: Jo Doe <> 102 | Co-authored-by: Sam Doe ' withUnixLineEndings 103 | ] 104 | 105 | { #category : #'tests - commit transform' } 106 | GitMigrationCommitInfoTest >> testCommittedDate [ 107 | "the DT should be +- equal (lets say less then 2 seconds)" 108 | 109 | self 110 | assert: 111 | (self commitInfoFor: self versionWithoutParent) committedDate asUnixTime 112 | - DateAndTime now rounded asUnixTime < 2 113 | ] 114 | 115 | { #category : #'tests - commit transform' } 116 | GitMigrationCommitInfoTest >> testCommitterEmail [ 117 | self 118 | assert: (self commitInfoFor: self versionWithoutParent) committerEmail 119 | equals: 'committerbot@example.com' 120 | ] 121 | 122 | { #category : #'tests - commit transform' } 123 | GitMigrationCommitInfoTest >> testCommitterName [ 124 | self assert: (self commitInfoFor: self versionWithoutParent) committerName equals: 'Committer Bot' 125 | ] 126 | 127 | { #category : #'tests - commit transform' } 128 | GitMigrationCommitInfoTest >> testDefaultBranch [ 129 | self assert: (self commitInfoFor: self versionWithoutParent) branch equals: 'master' 130 | ] 131 | 132 | { #category : #'instance creation' } 133 | GitMigrationCommitInfoTest >> versionWithCoauthors [ 134 | ^ MCVersion 135 | package: (MCPackage named: 'XYZ') 136 | info: 137 | (MCVersionInfo 138 | name: 'FastImported-ImportBot.5' 139 | id: UUID new 140 | message: 'Version with coauthors' 141 | date: (Date year: 2000 month: 12 day: 20) 142 | time: (Time hour: 16 minute: 20 second: 30) 143 | author: 'ImportBot' 144 | ancestors: #()) 145 | snapshot: 146 | (MCSnapshot 147 | fromDefinitions: 148 | {MCMethodDefinition 149 | className: 'Something' 150 | selector: #selector 151 | category: #'' 152 | timeStamp: 'SamDoe 1/31/2001 01:23' 153 | source: ''. 154 | MCMethodDefinition 155 | className: 'Something' 156 | selector: #otherSelector 157 | category: #'' 158 | timeStamp: 'JoDoe 2/31/2001 01:23' 159 | source: ''}) 160 | ] 161 | 162 | { #category : #'instance creation' } 163 | GitMigrationCommitInfoTest >> versionWithNull [ 164 | ^ MCVersion 165 | package: (MCPackage named: 'XYZ') 166 | info: 167 | (MCVersionInfo 168 | name: 'FastImported-ImportBot.1' 169 | id: UUID new 170 | message: 'Null here >' , Character null asString , '< and there >' , Character null asString , '<' 171 | date: (Date year: 2000 month: 12 day: 20) 172 | time: (Time hour: 16 minute: 20 second: 30) 173 | author: 'ImportBot' 174 | ancestors: #()) 175 | snapshot: MCSnapshot empty 176 | ] 177 | 178 | { #category : #'instance creation' } 179 | GitMigrationCommitInfoTest >> versionWithTwoAncestors [ 180 | ^ MCVersion new 181 | package: (MCPackage named: 'XYZ') 182 | info: 183 | (MCVersionInfo 184 | name: 'FastImported-ImportBot.3' 185 | id: UUID new 186 | message: 'merge' 187 | date: Date today 188 | time: Time now 189 | author: 'ImportBot' 190 | ancestors: 191 | {self versionWithoutParent. 192 | self versionWithoutParent}) 193 | snapshot: MCSnapshot empty 194 | ] 195 | 196 | { #category : #'instance creation' } 197 | GitMigrationCommitInfoTest >> versionWithoutParent [ 198 | ^ MCVersion 199 | package: (MCPackage named: 'XYZ') 200 | info: 201 | (MCVersionInfo 202 | name: 'FastImported-ImportBot.1' 203 | id: UUID new 204 | message: 'Initial MC commit' 205 | date: (Date year: 2000 month: 12 day: 20) 206 | time: (Time hour: 16 minute: 20 second: 30) 207 | author: 'ImportBot' 208 | ancestors: #()) 209 | snapshot: MCSnapshot empty 210 | ] 211 | -------------------------------------------------------------------------------- /repository/GitMigration/GitMigrationFastImportWriter.class.st: -------------------------------------------------------------------------------- 1 | " 2 | I convert MC commits into git commits with the use of git fast-import. 3 | During my operation I store the result into memory to avoid slow disk operations. 4 | " 5 | Class { 6 | #name : #GitMigrationFastImportWriter, 7 | #superclass : #Object, 8 | #instVars : [ 9 | 'authorMapping', 10 | 'committerName', 11 | 'initialCommit', 12 | 'availableVersions', 13 | 'exportDirectory', 14 | 'fastWriter', 15 | 'lastAuthors', 16 | 'onEmptyMessage' 17 | ], 18 | #category : #'GitMigration-FastImport' 19 | } 20 | 21 | { #category : #accessing } 22 | GitMigrationFastImportWriter >> authorMapping [ 23 | ^ authorMapping 24 | ] 25 | 26 | { #category : #accessing } 27 | GitMigrationFastImportWriter >> authorMapping: anObject [ 28 | authorMapping := anObject 29 | ] 30 | 31 | { #category : #accessing } 32 | GitMigrationFastImportWriter >> authorsInSnapshot: aSnapshot [ 33 | | timeStamps | 34 | timeStamps := OrderedCollection new. 35 | aSnapshot definitions 36 | do: [ :each | 37 | each isMethodDefinition 38 | ifTrue: [ timeStamps add: each timeStamp ]. 39 | (each isClassDefinition and: [ (each isKindOf: MCClassTraitDefinition) not ]) 40 | ifTrue: [ timeStamps add: each commentStamp ] ]. 41 | ^ (timeStamps collect: [ :each | each copyUpTo: Character space ]) \ #('' '') 42 | ] 43 | 44 | { #category : #accessing } 45 | GitMigrationFastImportWriter >> availableVersions: aCollection [ 46 | availableVersions := (aCollection collect: [ :each | each -> true ]) asDictionary 47 | ] 48 | 49 | { #category : #converting } 50 | GitMigrationFastImportWriter >> commitInfoFor: aVersion [ 51 | | snapshotAuthors packageName previousAuthors | 52 | packageName := aVersion package name. 53 | snapshotAuthors := self authorsInSnapshot: aVersion snapshot. 54 | previousAuthors := lastAuthors at: packageName ifAbsent: [ {} ]. 55 | lastAuthors at: packageName put: snapshotAuthors. 56 | ^ GitMigrationCommitInfo 57 | info: aVersion info 58 | mapping: authorMapping 59 | committer: (authorMapping at: committerName) 60 | coauthors: snapshotAuthors \ previousAuthors 61 | onEmptyMessage: onEmptyMessage 62 | ] 63 | 64 | { #category : #accessing } 65 | GitMigrationFastImportWriter >> commitMarkFor: aVersionInfo [ 66 | ^ fastWriter commitMarkFor: aVersionInfo 67 | ] 68 | 69 | { #category : #accessing } 70 | GitMigrationFastImportWriter >> commitMarks [ 71 | ^ fastWriter commitMarks 72 | ] 73 | 74 | { #category : #accessing } 75 | GitMigrationFastImportWriter >> committerName: aCommitterName [ 76 | committerName := aCommitterName 77 | ] 78 | 79 | { #category : #accessing } 80 | GitMigrationFastImportWriter >> exportDirectory [ 81 | ^ exportDirectory 82 | ] 83 | 84 | { #category : #accessing } 85 | GitMigrationFastImportWriter >> exportDirectory: anObject [ 86 | exportDirectory := anObject 87 | ] 88 | 89 | { #category : #accessing } 90 | GitMigrationFastImportWriter >> initialCommit: aString [ 91 | initialCommit := aString 92 | ] 93 | 94 | { #category : #initialization } 95 | GitMigrationFastImportWriter >> initialize [ 96 | super initialize. 97 | authorMapping := GitMigrationAuthorMapping new. 98 | committerName := Author fullName. 99 | exportDirectory := '/'. 100 | fastWriter := GitFastImportFileWriter new. 101 | lastAuthors := Dictionary new. 102 | onEmptyMessage := [ self error: 'No alternative message was provided' ] 103 | ] 104 | 105 | { #category : #initialization } 106 | GitMigrationFastImportWriter >> on: aStream [ 107 | fastWriter on: aStream. 108 | ] 109 | 110 | { #category : #accessing } 111 | GitMigrationFastImportWriter >> onEmptyMessage: aBlock [ 112 | onEmptyMessage := aBlock 113 | ] 114 | 115 | { #category : #writing } 116 | GitMigrationFastImportWriter >> writeDeletePackageFor: aVersion [ 117 | self subclassResponsibility 118 | ] 119 | 120 | { #category : #writing } 121 | GitMigrationFastImportWriter >> writeVersion: aVersion [ 122 | ^ self subclassResponsibility 123 | ] 124 | -------------------------------------------------------------------------------- /repository/GitMigration/GitMigrationFastImportWriterTest.class.st: -------------------------------------------------------------------------------- 1 | Class { 2 | #name : #GitMigrationFastImportWriterTest, 3 | #superclass : #TestCase, 4 | #instVars : [ 5 | 'stream', 6 | 'writer', 7 | 'authorMapping', 8 | 'committer' 9 | ], 10 | #category : 'GitMigration-Tests' 11 | } 12 | 13 | { #category : #testing } 14 | GitMigrationFastImportWriterTest class >> isAbstract [ 15 | ^ self = GitMigrationFastImportWriterTest 16 | ] 17 | 18 | { #category : #running } 19 | GitMigrationFastImportWriterTest >> ensureAllCached [ 20 | (MCCacheRepository uniqueInstance directory children 21 | noneSatisfy: [ :each | each basename = 'Somewhere-PeterUhnak.2.mcz' ]) 22 | ifTrue: [ (GitMigration on: 'peteruhnak/breaking-mcz') cacheAllVersions ] 23 | ] 24 | 25 | { #category : #running } 26 | GitMigrationFastImportWriterTest >> ensureGitFileTreeLoaded [ 27 | (Smalltalk hasClassNamed: #MCFileTreeGitRepository) 28 | ifFalse: [ (CatalogProvider projectNamed: 'GitFileTree') installVersion: #stable ] 29 | ] 30 | 31 | { #category : #running } 32 | GitMigrationFastImportWriterTest >> ensureTestRepoLoaded [ 33 | | repo | 34 | repo := MCSmalltalkhubRepository new. 35 | repo owner: 'peteruhnak'. 36 | repo project: 'breaking-mcz'. 37 | MCRepositoryGroup default addRepository: repo 38 | ] 39 | 40 | { #category : #running } 41 | GitMigrationFastImportWriterTest >> setUp [ 42 | super setUp. 43 | self timeLimit: 1 minute. 44 | self ensureTestRepoLoaded. 45 | " self ensureGitFileTreeLoaded." 46 | self ensureAllCached. 47 | stream := String new writeStream. 48 | authorMapping := GitMigrationAuthorMapping new 49 | shortName: 'ImportBot' name: 'Import Bot' email: 'importbot@example.com'; 50 | shortName: 'CommitterBot' name: 'Committer Bot' email: 'committerbot@example.com'. 51 | committer := authorMapping at: 'CommitterBot'. 52 | writer := self writerClass new on: stream. 53 | writer committerName: committer shortName. 54 | writer authorMapping: authorMapping 55 | ] 56 | 57 | { #category : #tests } 58 | GitMigrationFastImportWriterTest >> testWriteDeletePackage [ 59 | | contents v1 | 60 | MCCacheRepository uniqueInstance 61 | versionReaderForFileNamed: 'Somewhere-PeterUhnak.1.mcz' 62 | do: [ :v | v1 := v ]. 63 | writer writeDeletePackageFor: v1. 64 | contents := stream contents. 65 | self assert: contents equals: 'D Somewhere.package 66 | ' 67 | ] 68 | 69 | { #category : #tests } 70 | GitMigrationFastImportWriterTest >> testWriteDeletePackage2 [ 71 | | contents v1 | 72 | writer exportDirectory: 'repository'. 73 | MCCacheRepository uniqueInstance 74 | versionReaderForFileNamed: 'CoSomewhere-PeterUhnak.1.mcz' 75 | do: [ :v | v1 := v ]. 76 | writer writeDeletePackageFor: v1. 77 | contents := stream contents. 78 | self assert: contents equals: 'D repository/CoSomewhere.package 79 | ' 80 | ] 81 | 82 | { #category : #'tests - writing' } 83 | GitMigrationFastImportWriterTest >> testWriteInitialVersion [ 84 | | v1 contents dtNow | 85 | writer initialCommit: '1234567890'. 86 | writer authorMapping 87 | shortName: 'PeterUhnak' 88 | name: 'Peter Uhnak' 89 | email: 'i.uhnak@gmail.com'. 90 | MCCacheRepository uniqueInstance 91 | versionReaderForFileNamed: 'Somewhere-PeterUhnak.1.mcz' 92 | do: [ :v | v1 := v ]. 93 | writer writeVersion: v1. 94 | contents := stream contents. 95 | "hack to extract the DateAndTime now inside, which is not straightforward to test" 96 | dtNow := (contents lines fourth splitOn: '> ') last. 97 | self 98 | assert: (contents lines first: 8) 99 | equals: 100 | ('commit refs/heads/master 101 | mark :1 102 | author Peter Uhnak 1493283372 +0000 103 | committer Committer Bot {1} 104 | data 7 105 | initial 106 | from 1234567890 107 | D Somewhere.package' format: {dtNow}) lines 108 | ] 109 | 110 | { #category : #'tests - writing' } 111 | GitMigrationFastImportWriterTest >> testWriteVersion [ 112 | | v1 v2 | 113 | writer initialCommit: '1234567890'. 114 | writer authorMapping 115 | shortName: 'PeterUhnak' 116 | name: 'Peter Uhnak' 117 | email: 'i.uhnak@gmail.com'. 118 | MCCacheRepository uniqueInstance 119 | versionReaderForFileNamed: 'Somewhere-PeterUhnak.1.mcz' 120 | do: [ :v | v1 := v ]. 121 | MCCacheRepository uniqueInstance 122 | versionReaderForFileNamed: 'Somewhere-PeterUhnak.2.mcz' 123 | do: [ :v | v2 := v ]. 124 | writer availableVersions: (Array with: v1 info with: v2 info). 125 | writer writeVersion: v1. 126 | stream reset. 127 | writer writeVersion: v2. 128 | self 129 | assert: ((stream contents lines first: 7) copyWithoutIndex: 4) 130 | equals: 131 | #('commit refs/heads/master' 'mark :2' 'author Peter Uhnak 1493283388 +0000' 'data 5' 'qwrqw' 'D Somewhere.package') 132 | ] 133 | 134 | { #category : #'tests - writing' } 135 | GitMigrationFastImportWriterTest >> testWriteVersionDeletePackage [ 136 | | v1 v2 | 137 | writer exportDirectory: 'repository'. 138 | writer initialCommit: '1234567890'. 139 | writer authorMapping 140 | shortName: 'PeterUhnak' 141 | name: 'Peter Uhnak' 142 | email: 'i.uhnak@gmail.com'. 143 | MCCacheRepository uniqueInstance 144 | versionReaderForFileNamed: 'Somewhere-PeterUhnak.1.mcz' 145 | do: [ :v | v1 := v ]. 146 | MCCacheRepository uniqueInstance 147 | versionReaderForFileNamed: 'Somewhere-PeterUhnak.2.mcz' 148 | do: [ :v | v2 := v ]. 149 | writer availableVersions: (Array with: v1 info with: v2 info). 150 | writer writeVersion: v1. 151 | stream reset. 152 | writer writeVersion: v2. 153 | self assert: (stream contents lines includes: 'D repository/Somewhere.package') 154 | ] 155 | 156 | { #category : #'tests - writing' } 157 | GitMigrationFastImportWriterTest >> testWriteVersionDeletePackageTrimmed [ 158 | | v1 v2 | 159 | writer initialCommit: '1234567890'. 160 | writer authorMapping 161 | shortName: 'PeterUhnak' 162 | name: 'Peter Uhnak' 163 | email: 'i.uhnak@gmail.com'. 164 | MCCacheRepository uniqueInstance 165 | versionReaderForFileNamed: 'Somewhere-PeterUhnak.1.mcz' 166 | do: [ :v | v1 := v ]. 167 | MCCacheRepository uniqueInstance 168 | versionReaderForFileNamed: 'Somewhere-PeterUhnak.2.mcz' 169 | do: [ :v | v2 := v ]. 170 | writer availableVersions: (Array with: v1 info with: v2 info). 171 | writer writeVersion: v1. 172 | stream reset. 173 | writer writeVersion: v2. 174 | self assert: stream contents lines seventh equals: 'D Somewhere.package' 175 | ] 176 | 177 | { #category : #'tests - writing' } 178 | GitMigrationFastImportWriterTest >> testWriteVersionUnicode [ 179 | | v1 lines firstLine | 180 | writer initialCommit: '1234567890'. 181 | writer authorMapping 182 | shortName: 'PeterUhnak' 183 | name: 'Peter Uhnak' 184 | email: 'i.uhnak@gmail.com'. 185 | MCCacheRepository uniqueInstance 186 | versionReaderForFileNamed: 'CoSomewhere-PeterUhnak.5.mcz' 187 | do: [ :v | v1 := v ]. 188 | writer availableVersions: (Array with: v1 info). 189 | writer writeVersion: v1. 190 | lines := stream contents lines. 191 | firstLine := lines indexOf: (lines detect: [ :each | each includesSubstring: 'CoSomething.class/README.md' ]). 192 | self 193 | assert: 194 | {lines at: firstLine. 195 | lines at: firstLine + 1. 196 | lines at: firstLine + 2} 197 | equals: #('M 100644 inline CoSomewhere.package/CoSomething.class/README.md' 'data 6' '你好'). 198 | firstLine := lines indexOf: (lines detect: [ :each | each includesSubstring: 'CoSomething.class/instance/unicode.st' ]). 199 | self 200 | assert: 201 | {lines at: firstLine. 202 | lines at: firstLine + 1. 203 | lines at: firstLine + 3. 204 | lines at: firstLine + 4} 205 | equals: 206 | #('M 100644 inline CoSomewhere.package/CoSomething.class/instance/unicode.st' 'data 39' 'unicode' ' ^ ''彼得''') 207 | ] 208 | 209 | { #category : #'instance creation' } 210 | GitMigrationFastImportWriterTest >> versionWithTwoAncestors [ 211 | ^ MCVersionInfo 212 | name: 'FastImported-ImportBot.3' 213 | id: UUID new 214 | message: 'merge' 215 | date: Date today 216 | time: Time now 217 | author: 'ImportBot' 218 | ancestors: 219 | {self versionWithoutParent. 220 | self versionWithoutParent} 221 | ] 222 | 223 | { #category : #'instance creation' } 224 | GitMigrationFastImportWriterTest >> versionWithoutParent [ 225 | ^ MCVersionInfo 226 | name: 'FastImported-ImportBot.1' 227 | id: UUID new 228 | message: 'Initial MC commit' 229 | date: (Date year: 2000 month: 12 day: 20) 230 | time: (Time hour: 16 minute: 20 second: 30) 231 | author: 'ImportBot' 232 | ancestors: #() 233 | ] 234 | 235 | { #category : #accessing } 236 | GitMigrationFastImportWriterTest >> writerClass [ 237 | ^ self subclassResponsibility 238 | ] 239 | -------------------------------------------------------------------------------- /repository/GitMigration/GitMigrationFileTreeWriter.class.st: -------------------------------------------------------------------------------- 1 | Class { 2 | #name : #GitMigrationFileTreeWriter, 3 | #superclass : #GitMigrationFastImportWriter, 4 | #category : 'GitMigration-FastImport' 5 | } 6 | 7 | { #category : #writing } 8 | GitMigrationFileTreeWriter >> writeDeletePackageFor: aVersion [ 9 | | path | 10 | path := FileSystem unicodeMemory root / exportDirectory / aVersion package name , 'package'. 11 | fastWriter writeDeleteReference: path 12 | ] 13 | 14 | { #category : #writing } 15 | GitMigrationFileTreeWriter >> writeVersion: aVersion [ 16 | | repository memoryFileOut commitInfo | 17 | commitInfo := self commitInfoFor: aVersion. 18 | fastWriter writeCommitPreambleFor: commitInfo. 19 | (self commitMarkFor: commitInfo) = 1 20 | ifTrue: [ fastWriter writeLine: 'from ' , initialCommit ]. 21 | (memoryFileOut := (FileSystem store: GitFastImportMemoryStore new) root / exportDirectory) 22 | ensureCreateDirectory. 23 | repository := GitMigrationMemoryTreeGitRepository new. 24 | repository directory: memoryFileOut. 25 | repository memoryStoreVersion: aVersion. 26 | self writeDeletePackageFor: aVersion. 27 | fastWriter writeDirectoryTreeInlineFor: memoryFileOut 28 | ] 29 | -------------------------------------------------------------------------------- /repository/GitMigration/GitMigrationFileTreeWriterTest.class.st: -------------------------------------------------------------------------------- 1 | Class { 2 | #name : #GitMigrationFileTreeWriterTest, 3 | #superclass : #GitMigrationFastImportWriterTest, 4 | #category : 'GitMigration-Tests' 5 | } 6 | 7 | { #category : #tests } 8 | GitMigrationFileTreeWriterTest >> testWriteDeletePackage [ 9 | | contents v1 | 10 | MCCacheRepository uniqueInstance 11 | versionReaderForFileNamed: 'Somewhere-PeterUhnak.1.mcz' 12 | do: [ :v | v1 := v ]. 13 | writer writeDeletePackageFor: v1. 14 | contents := stream contents. 15 | self 16 | assert: contents 17 | equals: 18 | 'D Somewhere.package 19 | ' withUnixLineEndings 20 | ] 21 | 22 | { #category : #tests } 23 | GitMigrationFileTreeWriterTest >> testWriteDeletePackage2 [ 24 | | contents v1 | 25 | writer exportDirectory: 'repository'. 26 | MCCacheRepository uniqueInstance 27 | versionReaderForFileNamed: 'CoSomewhere-PeterUhnak.1.mcz' 28 | do: [ :v | v1 := v ]. 29 | writer writeDeletePackageFor: v1. 30 | contents := stream contents. 31 | self 32 | assert: contents 33 | equals: 34 | 'D repository/CoSomewhere.package 35 | ' withUnixLineEndings 36 | ] 37 | 38 | { #category : #'tests - writing' } 39 | GitMigrationFileTreeWriterTest >> testWriteInitialVersion [ 40 | | v1 contents dtNow | 41 | writer initialCommit: '1234567890'. 42 | writer authorMapping 43 | shortName: 'PeterUhnak' 44 | name: 'Peter Uhnak' 45 | email: 'i.uhnak@gmail.com'. 46 | MCCacheRepository uniqueInstance 47 | versionReaderForFileNamed: 'Somewhere-PeterUhnak.1.mcz' 48 | do: [ :v | v1 := v ]. 49 | writer writeVersion: v1. 50 | contents := stream contents. 51 | "hack to extract the DateAndTime now inside, which is not straightforward to test" 52 | dtNow := (contents lines fourth splitOn: '> ') last. 53 | self 54 | assert: (contents lines first: 8) 55 | equals: 56 | ('commit refs/heads/master 57 | mark :1 58 | author Peter Uhnak 1493283372 +0000 59 | committer Committer Bot {1} 60 | data 7 61 | initial 62 | from 1234567890 63 | D Somewhere.package' format: {dtNow}) lines 64 | ] 65 | 66 | { #category : #'tests - writing' } 67 | GitMigrationFileTreeWriterTest >> testWriteVersion [ 68 | | v1 v2 | 69 | writer initialCommit: '1234567890'. 70 | writer authorMapping 71 | shortName: 'PeterUhnak' 72 | name: 'Peter Uhnak' 73 | email: 'i.uhnak@gmail.com'. 74 | MCCacheRepository uniqueInstance 75 | versionReaderForFileNamed: 'Somewhere-PeterUhnak.1.mcz' 76 | do: [ :v | v1 := v ]. 77 | MCCacheRepository uniqueInstance 78 | versionReaderForFileNamed: 'Somewhere-PeterUhnak.2.mcz' 79 | do: [ :v | v2 := v ]. 80 | writer availableVersions: (Array with: v1 info with: v2 info). 81 | writer writeVersion: v1. 82 | stream reset. 83 | writer writeVersion: v2. 84 | self 85 | assert: ((stream contents lines first: 7) copyWithoutIndex: 4) 86 | equals: 87 | #('commit refs/heads/master' 'mark :2' 'author Peter Uhnak 1493283388 +0000' 'data 5' 'qwrqw' 'D Somewhere.package') 88 | ] 89 | 90 | { #category : #'tests - writing' } 91 | GitMigrationFileTreeWriterTest >> testWriteVersionDeletePackage [ 92 | | v1 v2 | 93 | writer exportDirectory: 'repository'. 94 | writer initialCommit: '1234567890'. 95 | writer authorMapping 96 | shortName: 'PeterUhnak' 97 | name: 'Peter Uhnak' 98 | email: 'i.uhnak@gmail.com'. 99 | MCCacheRepository uniqueInstance 100 | versionReaderForFileNamed: 'Somewhere-PeterUhnak.1.mcz' 101 | do: [ :v | v1 := v ]. 102 | MCCacheRepository uniqueInstance 103 | versionReaderForFileNamed: 'Somewhere-PeterUhnak.2.mcz' 104 | do: [ :v | v2 := v ]. 105 | writer availableVersions: (Array with: v1 info with: v2 info). 106 | writer writeVersion: v1. 107 | stream reset. 108 | writer writeVersion: v2. 109 | self assert: (stream contents lines includes: 'D repository/Somewhere.package') 110 | ] 111 | 112 | { #category : #'tests - writing' } 113 | GitMigrationFileTreeWriterTest >> testWriteVersionDeletePackageTrimmed [ 114 | | v1 v2 | 115 | writer initialCommit: '1234567890'. 116 | writer authorMapping 117 | shortName: 'PeterUhnak' 118 | name: 'Peter Uhnak' 119 | email: 'i.uhnak@gmail.com'. 120 | MCCacheRepository uniqueInstance 121 | versionReaderForFileNamed: 'Somewhere-PeterUhnak.1.mcz' 122 | do: [ :v | v1 := v ]. 123 | MCCacheRepository uniqueInstance 124 | versionReaderForFileNamed: 'Somewhere-PeterUhnak.2.mcz' 125 | do: [ :v | v2 := v ]. 126 | writer availableVersions: (Array with: v1 info with: v2 info). 127 | writer writeVersion: v1. 128 | stream reset. 129 | writer writeVersion: v2. 130 | self assert: stream contents lines seventh equals: 'D Somewhere.package' 131 | ] 132 | 133 | { #category : #'tests - writing' } 134 | GitMigrationFileTreeWriterTest >> testWriteVersionUnicode [ 135 | | v1 lines firstLine | 136 | writer initialCommit: '1234567890'. 137 | writer authorMapping 138 | shortName: 'PeterUhnak' 139 | name: 'Peter Uhnak' 140 | email: 'i.uhnak@gmail.com'. 141 | MCCacheRepository uniqueInstance 142 | versionReaderForFileNamed: 'CoSomewhere-PeterUhnak.5.mcz' 143 | do: [ :v | v1 := v ]. 144 | writer availableVersions: (Array with: v1 info). 145 | writer writeVersion: v1. 146 | lines := stream contents lines. 147 | firstLine := lines indexOf: (lines detect: [ :each | each includesSubstring: 'CoSomething.class/README.md' ]). 148 | self 149 | assert: 150 | {lines at: firstLine. 151 | lines at: firstLine + 1. 152 | lines at: firstLine + 2} 153 | equals: #('M 100644 inline CoSomewhere.package/CoSomething.class/README.md' 'data 6' '你好'). 154 | firstLine := lines indexOf: (lines detect: [ :each | each includesSubstring: 'CoSomething.class/instance/unicode.st' ]). 155 | self 156 | assert: 157 | {lines at: firstLine. 158 | lines at: firstLine + 1. 159 | lines at: firstLine + 3. 160 | lines at: firstLine + 4} 161 | equals: 162 | #('M 100644 inline CoSomewhere.package/CoSomething.class/instance/unicode.st' 'data 39' 'unicode' ' ^ ''彼得''') 163 | ] 164 | 165 | { #category : #accessing } 166 | GitMigrationFileTreeWriterTest >> writerClass [ 167 | ^ GitMigrationFileTreeWriter 168 | ] 169 | -------------------------------------------------------------------------------- /repository/GitMigration/GitMigrationMemoryTreeGitRepository.class.st: -------------------------------------------------------------------------------- 1 | " 2 | I modify my parent to store the code into a memory. Additionally I don't perform any actual commits -- that's for FastImport to do. 3 | " 4 | Class { 5 | #name : #GitMigrationMemoryTreeGitRepository, 6 | #superclass : #MCFileTreeRepository, 7 | #category : 'GitMigration-FileSystem' 8 | } 9 | 10 | { #category : #storing } 11 | GitMigrationMemoryTreeGitRepository >> memoryStoreVersion: aVersion [ 12 | "Dump the mcz contents into a memory filetree without performing any commits." 13 | 14 | | packageDirectoryString | 15 | IceMetadatalessFileTreeWriter fileOut: aVersion on: self. 16 | packageDirectoryString := (self class parseName: aVersion info name) first 17 | , self packageExtension. 18 | aVersion dependencies notEmpty 19 | ifTrue: [ self 20 | writeGitFileTreeProperties: (self fileUtils directoryFromPath: packageDirectoryString relativeTo: directory) ] 21 | ] 22 | -------------------------------------------------------------------------------- /repository/GitMigration/GitMigrationTest.class.st: -------------------------------------------------------------------------------- 1 | Class { 2 | #name : #GitMigrationTest, 3 | #superclass : #TestCase, 4 | #instVars : [ 5 | 'migration' 6 | ], 7 | #category : 'GitMigration-Tests' 8 | } 9 | 10 | { #category : #running } 11 | GitMigrationTest >> ensureAllCached [ 12 | (MCCacheRepository uniqueInstance directory children 13 | noneSatisfy: [ :each | each basename = 'Somewhere-PeterUhnak.2.mcz' ]) 14 | ifTrue: [ migration cacheAllVersions ] 15 | ] 16 | 17 | { #category : #running } 18 | GitMigrationTest >> ensureGitFileTreeLoaded [ 19 | (Smalltalk hasClassNamed: #MCFileTreeGitRepository) 20 | ifFalse: [ (CatalogProvider projectNamed: 'GitFileTree') installVersion: #stable ] 21 | ] 22 | 23 | { #category : #running } 24 | GitMigrationTest >> ensureTestRepoLoaded [ 25 | | repo | 26 | repo := MCSmalltalkhubRepository new. 27 | repo owner: 'peteruhnak'. 28 | repo project: 'breaking-mcz'. 29 | MCRepositoryGroup default addRepository: repo 30 | ] 31 | 32 | { #category : #'tests - accessing' } 33 | GitMigrationTest >> mockVersionsForAllAuthors [ 34 | migration 35 | writeSlotNamed: #versionsCache 36 | value: 37 | {MCVersion 38 | package: (MCPackage named: 'XYZ') 39 | info: 40 | (MCVersionInfo 41 | name: 'Package-CommitAuthor.1' 42 | id: UUID new 43 | message: 'commit' 44 | date: Date today 45 | time: Time now 46 | author: 'CommitAuthor' 47 | ancestors: #()) 48 | snapshot: 49 | (MCSnapshot 50 | fromDefinitions: 51 | {MCMethodDefinition 52 | className: 'Something' 53 | selector: #selector 54 | category: #'' 55 | timeStamp: 'MethodAuthor 1/31/2001 01:23' 56 | source: ''})} asOrderedCollection 57 | ] 58 | 59 | { #category : #running } 60 | GitMigrationTest >> setUp [ 61 | super setUp. 62 | self timeLimit: 1 minute. 63 | self ensureTestRepoLoaded. 64 | " self ensureGitFileTreeLoaded." 65 | migration := GitMigration on: 'peteruhnak/breaking-mcz'. 66 | self ensureAllCached 67 | ] 68 | 69 | { #category : #'tests - accessing' } 70 | GitMigrationTest >> testAllAuthors [ 71 | | oldName | 72 | oldName := Author fullName. 73 | [ Author fullName: 'LocalUser'. 74 | self assert: (migration allAuthors includes: 'LocalUser') ] 75 | ensure: [ Author fullName: oldName ] 76 | ] 77 | 78 | { #category : #'tests - accessing' } 79 | GitMigrationTest >> testAllAuthorsContainsLocal [ 80 | | oldName | 81 | oldName := Author fullName. 82 | [ Author fullName: 'LocalUser'. 83 | self assert: (migration allAuthors includes: 'LocalUser') ] 84 | ensure: [ Author fullName: oldName ] 85 | ] 86 | 87 | { #category : #'tests - accessing' } 88 | GitMigrationTest >> testAllAuthorsReadsCommitAuthor [ 89 | self mockVersionsForAllAuthors. 90 | self assert: (migration allAuthors includes: 'CommitAuthor') 91 | ] 92 | 93 | { #category : #'tests - accessing' } 94 | GitMigrationTest >> testAllAuthorsReadsSnapshot [ 95 | self mockVersionsForAllAuthors. 96 | self assert: (migration allAuthors includes: 'MethodAuthor') 97 | ] 98 | 99 | { #category : #'tests - accessing' } 100 | GitMigrationTest >> testAuthorsOk [ 101 | self 102 | shouldnt: [ migration authors: {'PeterUhnak' -> #('Peter Uhnak' '')} ] 103 | raise: NotFound 104 | ] 105 | 106 | { #category : #'tests - actions' } 107 | GitMigrationTest >> testCacheAllVersions [ 108 | | version | 109 | migration cacheAllVersions. 110 | version := MCCacheRepository uniqueInstance 111 | versionInfoFromFileNamed: 'Somewhere-PeterUhnak.2.mcz'. 112 | self assert: version name equals: 'Somewhere-PeterUhnak.2' 113 | ] 114 | 115 | { #category : #'tests - accessing' } 116 | GitMigrationTest >> testCommitOrder [ 117 | | commitOrder | 118 | commitOrder := migration commitOrder. 119 | self 120 | assert: (commitOrder collect: [ :each | (each name splitOn: '.') last asNumber ]) asArray 121 | equals: #(1 2 4 3 1 8 4 15 5 6 9 5 6 10 1) 122 | ] 123 | 124 | { #category : #'tests - accessing' } 125 | GitMigrationTest >> testCompleteAncestry [ 126 | | result | 127 | result := migration completeAncestryOfPackageNamed: 'CoSomewhere'. 128 | self 129 | assert: (result collect: #name) asArray sorted 130 | equals: #('CoSomewhere-PeterUhnak.1' 'CoSomewhere-PeterUhnak.4' 'CoSomewhere-PeterUhnak.5' 'CoSomewhere-PeterUhnak.6') 131 | ] 132 | 133 | { #category : #'tests - actions' } 134 | GitMigrationTest >> testCompleteAncestry2 [ 135 | | ancestry | 136 | ancestry := migration completeAncestryOfPackageNamed: 'Somewhere'. 137 | self 138 | assert: (ancestry collect: [ :each | (each name splitOn: '.') last asNumber ]) asArray sorted 139 | equals: #(1 2 4 3 8 15 5 6 7 9 10) sorted 140 | ] 141 | 142 | { #category : #'tests - accessing' } 143 | GitMigrationTest >> testCompleteAncestryTrimmed [ 144 | "https://github.com/peteruhnak/git-migration/issues/13" 145 | 146 | | result brokenVersion brokenAncestor | 147 | migration populateCaches. 148 | brokenVersion := migration versions 149 | detect: [ :each | each info name = 'CoSomewhere-PeterUhnak.5' ]. 150 | brokenAncestor := MCVersionInfo 151 | name: '' 152 | id: UUID new 153 | message: 'I am broken' 154 | date: '' 155 | time: '' 156 | author: '' 157 | ancestors: #(). 158 | brokenVersion info setAncestors: brokenVersion info ancestors , {brokenAncestor}. 159 | result := migration completeAncestryOfPackageNamed: 'CoSomewhere'. 160 | self 161 | assert: (result collect: #name) asArray sorted 162 | equals: 163 | #('CoSomewhere-PeterUhnak.1' 'CoSomewhere-PeterUhnak.4' 'CoSomewhere-PeterUhnak.5' 'CoSomewhere-PeterUhnak.6') 164 | ] 165 | 166 | { #category : #'tests - retrieving' } 167 | GitMigrationTest >> testFindRepository [ 168 | | repo | 169 | "fuel should be in the image by default afaik" 170 | repo := migration findRepositoryNamed: 'peteruhnak/breaking-mcz'. 171 | self assert: repo isNotNil. 172 | self assert: repo owner equals: 'peteruhnak'. 173 | self assert: repo project equals: 'breaking-mcz' 174 | ] 175 | 176 | { #category : #'tests - retrieving' } 177 | GitMigrationTest >> testIgnoreFileNames [ 178 | | versions | 179 | versions := migration versionsToMigrate. 180 | self assert: (versions anySatisfy: [ :each | each info name = 'Somewhere-PeterUhnak.2' ]). 181 | self assert: (versions anySatisfy: [ :each | each info name = 'Somewhere-PeterUhnak.3' ]). 182 | migration ignoredFileNames: #('Somewhere-PeterUhnak.2' 'Somewhere-PeterUhnak.3.mcz'). 183 | versions := migration versionsToMigrate. 184 | self deny: (versions anySatisfy: [ :each | each info name = 'Somewhere-PeterUhnak.2' ]). 185 | self deny: (versions anySatisfy: [ :each | each info name = 'Somewhere-PeterUhnak.3' ]) 186 | ] 187 | 188 | { #category : #'tests - topology sorting' } 189 | GitMigrationTest >> testIsRoot [ 190 | self assert: (migration isRoot: 1 in: {1 -> 2}). 191 | self deny: (migration isRoot: 1 in: {2 -> 1}) 192 | ] 193 | 194 | { #category : #'tests - retrieving' } 195 | GitMigrationTest >> testOn [ 196 | self assert: migration repository isNotNil. 197 | self assert: migration repository owner equals: 'peteruhnak'. 198 | self assert: migration repository project equals: 'breaking-mcz' 199 | ] 200 | 201 | { #category : #'tests - actions' } 202 | GitMigrationTest >> testTopologicallySort [ 203 | | ancestry sorted | 204 | ancestry := migration completeAncestryOfPackageNamed: 'Somewhere'. 205 | sorted := migration topologicallySort: ancestry. 206 | self 207 | assert: (sorted collect: [ :each | (each name splitOn: '.') last asNumber ]) asArray 208 | equals: #(1 2 4 3 8 15 5 6 7 9 10) 209 | ] 210 | 211 | { #category : #'tests - retrieving' } 212 | GitMigrationTest >> testVersionInfo [ 213 | | version | 214 | migration cacheAllVersions. 215 | version := migration versionInfoFromFileNamed: 'Somewhere-PeterUhnak.2.mcz'. 216 | self assert: version name equals: 'Somewhere-PeterUhnak.2' 217 | ] 218 | 219 | { #category : #'tests - accessing' } 220 | GitMigrationTest >> testVersionsByPackage [ 221 | self assert: migration versionsByPackage keys sorted equals: #(CoSomewhere EverythingIsBurning Somewhere) 222 | ] 223 | 224 | { #category : #'tests - accessing' } 225 | GitMigrationTest >> testVersionsByPackageWithFilter [ 226 | migration selectedPackageNames: #(CoSomewhere). 227 | self assert: migration versionsByPackage keys sorted equals: #(CoSomewhere) 228 | ] 229 | -------------------------------------------------------------------------------- /repository/GitMigration/GitMigrationTonelWriter.class.st: -------------------------------------------------------------------------------- 1 | Class { 2 | #name : #GitMigrationTonelWriter, 3 | #superclass : #GitMigrationFastImportWriter, 4 | #category : 'GitMigration-FastImport' 5 | } 6 | 7 | { #category : #'writing - memory' } 8 | GitMigrationTonelWriter >> copyPackageIn: aVersion toStore: memoryStore [ 9 | self ensureContainsOrganization: aVersion. 10 | TonelWriter fileOut: aVersion on: memoryStore. 11 | self writePropertiesFileTo: memoryStore 12 | ] 13 | 14 | { #category : #writing } 15 | GitMigrationTonelWriter >> ensureContainsOrganization: aVersion [ 16 | aVersion snapshot definitions 17 | detect: #isOrganizationDefinition 18 | ifFound: [ :each | each ] 19 | ifNone: [ aVersion snapshot definitions add: (MCOrganizationDefinition categories: {aVersion package name}) ] 20 | ] 21 | 22 | { #category : #writing } 23 | GitMigrationTonelWriter >> newMemoryStore [ 24 | ^ FileSystem unicodeMemory root ensureCreateDirectory 25 | ] 26 | 27 | { #category : #writing } 28 | GitMigrationTonelWriter >> writeDeletePackageFor: aVersion [ 29 | fastWriter 30 | writeDeleteReference: FileSystem unicodeMemory root / exportDirectory / aVersion package name 31 | ] 32 | 33 | { #category : #writing } 34 | GitMigrationTonelWriter >> writeProjectFileTo: aDirectory [ 35 | (aDirectory parent / '.project') 36 | ensureDelete; 37 | writeStreamDo: [ :stream | 38 | (STONWriter on: stream) 39 | prettyPrint: true; 40 | newLine: OSPlatform current lineEnding; 41 | nextPut: {'srcDirectory' -> aDirectory basename} asDictionary ] 42 | ] 43 | 44 | { #category : #'writing - memory' } 45 | GitMigrationTonelWriter >> writePropertiesFileTo: aDirectory [ 46 | (aDirectory / IceRepositoryProperties propertiesFileName) 47 | ensureDelete; 48 | writeStreamDo: [ :stream | 49 | (STONWriter on: stream) 50 | prettyPrint: true; 51 | newLine: OSPlatform current lineEnding; 52 | nextPut: {#format -> #tonel} asDictionary ] 53 | ] 54 | 55 | { #category : #writing } 56 | GitMigrationTonelWriter >> writeVersion: aVersion [ 57 | | commitInfo memoryStore | 58 | commitInfo := self commitInfoFor: aVersion. 59 | fastWriter writeCommitPreambleFor: commitInfo. 60 | (self commitMarkFor: commitInfo) = 1 61 | ifTrue: [ fastWriter writeLine: 'from ' , initialCommit ]. 62 | memoryStore := self newMemoryStore / exportDirectory. 63 | self writeDeletePackageFor: aVersion. 64 | self copyPackageIn: aVersion toStore: memoryStore. 65 | self writeProjectFileTo: memoryStore. 66 | fastWriter writeDirectoryTreeInlineFor: memoryStore parent 67 | ] 68 | -------------------------------------------------------------------------------- /repository/GitMigration/GitMigrationTonelWriterTest.class.st: -------------------------------------------------------------------------------- 1 | Class { 2 | #name : #GitMigrationTonelWriterTest, 3 | #superclass : #GitMigrationFastImportWriterTest, 4 | #category : 'GitMigration-Tests' 5 | } 6 | 7 | { #category : #'tests - writing' } 8 | GitMigrationTonelWriterTest >> testEnsureContainsOrganization [ 9 | | version store | 10 | "https://github.com/peteruhnak/git-migration/issues/16" 11 | store := writer newMemoryStore. 12 | version := MCVersion 13 | package: (MCPackage named: 'XYZ') 14 | info: self versionWithoutParent 15 | snapshot: (MCSnapshot fromDefinitions: OrderedCollection new). 16 | writer copyPackageIn: version toStore: store. 17 | self 18 | assert: (store / 'XYZ' / 'package.st') contents trimmed 19 | equals: 'Package { #name : #XYZ }' 20 | ] 21 | 22 | { #category : #tests } 23 | GitMigrationTonelWriterTest >> testWriteDeletePackage [ 24 | | contents v1 | 25 | MCCacheRepository uniqueInstance 26 | versionReaderForFileNamed: 'Somewhere-PeterUhnak.1.mcz' 27 | do: [ :v | v1 := v ]. 28 | writer writeDeletePackageFor: v1. 29 | contents := stream contents. 30 | self assert: contents equals: 'D Somewhere 31 | ' withUnixLineEndings 32 | ] 33 | 34 | { #category : #tests } 35 | GitMigrationTonelWriterTest >> testWriteDeletePackage2 [ 36 | | contents v1 | 37 | writer exportDirectory: 'repository'. 38 | MCCacheRepository uniqueInstance 39 | versionReaderForFileNamed: 'CoSomewhere-PeterUhnak.1.mcz' 40 | do: [ :v | v1 := v ]. 41 | writer writeDeletePackageFor: v1. 42 | contents := stream contents. 43 | self assert: contents equals: 'D repository/CoSomewhere 44 | ' withUnixLineEndings 45 | ] 46 | 47 | { #category : #'tests - writing' } 48 | GitMigrationTonelWriterTest >> testWriteInitialVersion [ 49 | | v1 contents dtNow | 50 | writer initialCommit: '1234567890'. 51 | writer authorMapping 52 | shortName: 'PeterUhnak' 53 | name: 'Peter Uhnak' 54 | email: 'i.uhnak@gmail.com'. 55 | MCCacheRepository uniqueInstance 56 | versionReaderForFileNamed: 'Somewhere-PeterUhnak.1.mcz' 57 | do: [ :v | v1 := v ]. 58 | writer writeVersion: v1. 59 | contents := stream contents. 60 | "hack to extract the DateAndTime now inside, which is not straightforward to test" 61 | dtNow := (contents lines fourth splitOn: '> ') last. 62 | self 63 | assert: (contents lines first: 8) 64 | equals: 65 | ('commit refs/heads/master 66 | mark :1 67 | author Peter Uhnak 1493283372 +0000 68 | committer Committer Bot {1} 69 | data 7 70 | initial 71 | from 1234567890 72 | D Somewhere' format: {dtNow}) lines 73 | ] 74 | 75 | { #category : #'tests - writing' } 76 | GitMigrationTonelWriterTest >> testWriteInitialVersionEmptyMessage [ 77 | | v1 contents dtNow | 78 | writer initialCommit: '1234567890'. 79 | writer authorMapping 80 | shortName: 'PeterUhnak' 81 | name: 'Peter Uhnak' 82 | email: 'i.uhnak@gmail.com'. 83 | MCCacheRepository uniqueInstance 84 | versionReaderForFileNamed: 'EverythingIsBurning-PeterUhnak.1.mcz' 85 | do: [ :v | v1 := v ]. 86 | writer onEmptyMessage: [ :info | 'replacement message for >' , info name , '<' ]. 87 | writer writeVersion: v1. 88 | contents := stream contents. 89 | "hack to extract the DateAndTime now inside, which is not straightforward to test" 90 | dtNow := (contents lines fourth splitOn: '> ') last. 91 | self 92 | assert: (contents lines first: 8) 93 | equals: 94 | ('commit refs/heads/master 95 | mark :1 96 | author Peter Uhnak 1538689956 +0000 97 | committer Committer Bot {1} 98 | data 58 99 | replacement message for >EverythingIsBurning-PeterUhnak.1< 100 | from 1234567890 101 | D EverythingIsBurning' format: {dtNow}) lines 102 | ] 103 | 104 | { #category : #'tests - writing' } 105 | GitMigrationTonelWriterTest >> testWriteInitialVersionEmptyMessageError [ 106 | | v1 | 107 | writer initialCommit: '1234567890'. 108 | writer authorMapping shortName: 'PeterUhnak' name: 'Peter Uhnak' email: 'i.uhnak@gmail.com'. 109 | MCCacheRepository uniqueInstance 110 | versionReaderForFileNamed: 'EverythingIsBurning-PeterUhnak.1.mcz' 111 | do: [ :v | v1 := v ]. 112 | self should: [ writer writeVersion: v1 ] raise: Error 113 | ] 114 | 115 | { #category : #'tests - writing' } 116 | GitMigrationTonelWriterTest >> testWriteProject [ 117 | | v1 | 118 | writer exportDirectory: 'repository'. 119 | writer initialCommit: '1234567890'. 120 | writer authorMapping 121 | shortName: 'PeterUhnak' 122 | name: 'Peter Uhnak' 123 | email: 'i.uhnak@gmail.com'. 124 | MCCacheRepository uniqueInstance 125 | versionReaderForFileNamed: 'Somewhere-PeterUhnak.1.mcz' 126 | do: [ :v | v1 := v ]. 127 | writer availableVersions: (Array with: v1 info). 128 | writer writeVersion: v1. 129 | self deny: (stream contents lines includes: 'M 100644 inline repository/.project'). 130 | self assert: (stream contents lines includes: 'M 100644 inline .project'). 131 | self assert: (stream contents lines includes: ' ''srcDirectory'' : ''repository'''). 132 | ] 133 | 134 | { #category : #'tests - writing' } 135 | GitMigrationTonelWriterTest >> testWriteProperties [ 136 | | v1 | 137 | writer exportDirectory: 'repository'. 138 | writer initialCommit: '1234567890'. 139 | writer authorMapping 140 | shortName: 'PeterUhnak' 141 | name: 'Peter Uhnak' 142 | email: 'i.uhnak@gmail.com'. 143 | MCCacheRepository uniqueInstance 144 | versionReaderForFileNamed: 'Somewhere-PeterUhnak.1.mcz' 145 | do: [ :v | v1 := v ]. 146 | writer availableVersions: (Array with: v1 info). 147 | writer writeVersion: v1. 148 | self assert: (stream contents lines includes: 'M 100644 inline repository/.properties') 149 | ] 150 | 151 | { #category : #'tests - writing' } 152 | GitMigrationTonelWriterTest >> testWriteVersion [ 153 | | v1 v2 | 154 | writer initialCommit: '1234567890'. 155 | writer authorMapping 156 | shortName: 'PeterUhnak' 157 | name: 'Peter Uhnak' 158 | email: 'i.uhnak@gmail.com'. 159 | MCCacheRepository uniqueInstance 160 | versionReaderForFileNamed: 'Somewhere-PeterUhnak.1.mcz' 161 | do: [ :v | v1 := v ]. 162 | MCCacheRepository uniqueInstance 163 | versionReaderForFileNamed: 'Somewhere-PeterUhnak.2.mcz' 164 | do: [ :v | v2 := v ]. 165 | writer availableVersions: (Array with: v1 info with: v2 info). 166 | writer writeVersion: v1. 167 | stream reset. 168 | writer writeVersion: v2. 169 | self 170 | assert: ((stream contents lines first: 7) copyWithoutIndex: 4) 171 | equals: 172 | #('commit refs/heads/master' 'mark :2' 'author Peter Uhnak 1493283388 +0000' 'data 5' 'qwrqw' 'D Somewhere') 173 | ] 174 | 175 | { #category : #'tests - writing' } 176 | GitMigrationTonelWriterTest >> testWriteVersionDeletePackage [ 177 | | v1 v2 | 178 | writer exportDirectory: 'repository'. 179 | writer initialCommit: '1234567890'. 180 | writer authorMapping 181 | shortName: 'PeterUhnak' 182 | name: 'Peter Uhnak' 183 | email: 'i.uhnak@gmail.com'. 184 | MCCacheRepository uniqueInstance 185 | versionReaderForFileNamed: 'Somewhere-PeterUhnak.1.mcz' 186 | do: [ :v | v1 := v ]. 187 | MCCacheRepository uniqueInstance 188 | versionReaderForFileNamed: 'Somewhere-PeterUhnak.2.mcz' 189 | do: [ :v | v2 := v ]. 190 | writer availableVersions: (Array with: v1 info with: v2 info). 191 | writer writeVersion: v1. 192 | stream reset. 193 | writer writeVersion: v2. 194 | self assert: (stream contents lines includes: 'D repository/Somewhere') 195 | ] 196 | 197 | { #category : #'tests - writing' } 198 | GitMigrationTonelWriterTest >> testWriteVersionDeletePackageTrimmed [ 199 | | v1 v2 | 200 | writer initialCommit: '1234567890'. 201 | writer authorMapping 202 | shortName: 'PeterUhnak' 203 | name: 'Peter Uhnak' 204 | email: 'i.uhnak@gmail.com'. 205 | MCCacheRepository uniqueInstance 206 | versionReaderForFileNamed: 'Somewhere-PeterUhnak.1.mcz' 207 | do: [ :v | v1 := v ]. 208 | MCCacheRepository uniqueInstance 209 | versionReaderForFileNamed: 'Somewhere-PeterUhnak.2.mcz' 210 | do: [ :v | v2 := v ]. 211 | writer availableVersions: (Array with: v1 info with: v2 info). 212 | writer writeVersion: v1. 213 | stream reset. 214 | writer writeVersion: v2. 215 | self assert: stream contents lines seventh equals: 'D Somewhere' 216 | ] 217 | 218 | { #category : #'tests - writing' } 219 | GitMigrationTonelWriterTest >> testWriteVersionUnicode [ 220 | | v1 lines firstLine | 221 | writer initialCommit: '1234567890'. 222 | writer authorMapping 223 | shortName: 'PeterUhnak' 224 | name: 'Peter Uhnak' 225 | email: 'i.uhnak@gmail.com'. 226 | MCCacheRepository uniqueInstance 227 | versionReaderForFileNamed: 'CoSomewhere-PeterUhnak.5.mcz' 228 | do: [ :v | v1 := v ]. 229 | writer availableVersions: (Array with: v1 info). 230 | writer writeVersion: v1. 231 | lines := stream contents lines. 232 | firstLine := lines 233 | indexOf: (lines detect: [ :each | each includesSubstring: 'CoSomething.class.st' ]). 234 | self 235 | assert: ((lines copyFrom: firstLine to: firstLine + 4) copyWithoutIndex: 2) 236 | equals: #('M 100644 inline CoSomewhere/CoSomething.class.st' '"' '你好' '"'). 237 | firstLine := lines 238 | indexOf: (lines detect: [ :each | each includesSubstring: 'CoSomething >> unicode [' ]). 239 | self 240 | assert: (lines copyFrom: firstLine to: firstLine + 2) 241 | equals: #('CoSomething >> unicode [' ' ^ ''彼得''' ']') 242 | ] 243 | 244 | { #category : #accessing } 245 | GitMigrationTonelWriterTest >> writerClass [ 246 | ^ GitMigrationTonelWriter 247 | ] 248 | -------------------------------------------------------------------------------- /repository/GitMigration/GitMigrationVisualization.class.st: -------------------------------------------------------------------------------- 1 | " 2 | I visualize an mcz history. 3 | " 4 | Class { 5 | #name : #GitMigrationVisualization, 6 | #superclass : #Object, 7 | #instVars : [ 8 | 'migration' 9 | ], 10 | #category : 'GitMigration' 11 | } 12 | 13 | { #category : #'instance creation' } 14 | GitMigrationVisualization class >> on: aProjectName [ 15 | ^ self new 16 | projectName: aProjectName; 17 | yourself 18 | ] 19 | 20 | { #category : #accessing } 21 | GitMigrationVisualization >> migration [ 22 | ^ migration 23 | ] 24 | 25 | { #category : #accessing } 26 | GitMigrationVisualization >> migration: anObject [ 27 | migration := anObject 28 | ] 29 | 30 | { #category : #colors } 31 | GitMigrationVisualization >> missingColor [ 32 | ^ Color magenta 33 | ] 34 | 35 | { #category : #colors } 36 | GitMigrationVisualization >> rootColor [ 37 | ^ Color yellow 38 | ] 39 | 40 | { #category : #'visualizations - wrappers' } 41 | GitMigrationVisualization >> showAncestryTopologyOnPackageNamed: aPackageName [ 42 | | view | 43 | view := RTView new. 44 | view @ RTZoomableView @ RTDraggableView. 45 | ^ self showAncestryTopologyOnPackageNamed: aPackageName inView: view 46 | ] 47 | 48 | { #category : #visualizations } 49 | GitMigrationVisualization >> showAncestryTopologyOnPackageNamed: aPackageName inView: aView [ 50 | | versions sorted realRoots realTails b | 51 | versions := migration versionsByPackage at: aPackageName. 52 | sorted := migration topologicallySort: (migration completeAncestryOfPackageNamed: aPackageName). 53 | realRoots := sorted 54 | select: [ :version | sorted noneSatisfy: [ :each | each ancestors includes: version ] ]. 55 | realTails := sorted select: [ :each | each ancestors isEmpty ]. 56 | b := RTMondrian new. 57 | b view: aView. 58 | b shape text 59 | text: [ :each | 60 | each name , String cr , each timeStamp printString , String cr 61 | , (sorted indexOf: each) printString ]; 62 | fillColor: Color black; 63 | if: [ :each | (versions includes: each) not ] fillColor: self missingColor; 64 | if: [ :each | realRoots includes: each ] fillColor: self tailColor; 65 | if: [ :each | realTails includes: each ] fillColor: self rootColor. 66 | b nodes: sorted. 67 | b edges shape arrowedLine head: RTEmptyNarrowArrow asHead. 68 | b edges connectFromAll: #ancestors. 69 | b layout dominanceTree horizontalGap: 30. 70 | b build. 71 | ^ b view 72 | ] 73 | 74 | { #category : #'visualizations - wrappers' } 75 | GitMigrationVisualization >> showProjectAncestry [ 76 | ^ self showProjectAncestryWithLabels: false 77 | ] 78 | 79 | { #category : #'visualizations - wrappers' } 80 | GitMigrationVisualization >> showProjectAncestryOn: aCollectionOfPackages [ 81 | ^ self showProjectAncestryOn: aCollectionOfPackages withLabels: false 82 | ] 83 | 84 | { #category : #visualizations } 85 | GitMigrationVisualization >> showProjectAncestryOn: aCollectionOfPackages withLabels: hasLabels [ 86 | | b allVersions sorted | 87 | b := RTMondrian new. 88 | b shape box 89 | fillColor: Color transparent; 90 | borderColor: Color black. 91 | b 92 | nodes: 93 | (migration versionsByPackage associations 94 | select: [ :pair | aCollectionOfPackages includes: pair key ]) 95 | forEach: [ :pair | 96 | | pkgName versions realRoots realTails | 97 | pkgName := pair key. 98 | versions := pair value. 99 | allVersions := migration completeAncestryOfPackageNamed: pkgName. 100 | sorted := migration topologicallySort: allVersions. 101 | realRoots := sorted 102 | select: [ :version | sorted noneSatisfy: [ :each | each ancestors includes: version ] ]. 103 | realTails := sorted select: [ :each | each ancestors isEmpty ]. 104 | hasLabels 105 | ifTrue: [ b shape text 106 | text: [ :each | 107 | each name , String cr , each timeStamp truncated printString , String cr , (each message truncateTo: 20) 108 | , ' (' , each versionNumber asString , ')' , String cr 109 | , (sorted indexOf: each) printString ] ] 110 | ifFalse: [ b shape box 111 | color: Color veryLightGray; 112 | size: 20 ]. 113 | b shape 114 | fillColor: Color black; 115 | if: [ :each | (versions includes: each) not ] fillColor: self missingColor; 116 | if: [ :each | realRoots includes: each ] fillColor: self tailColor; 117 | if: [ :each | realTails includes: each ] fillColor: self rootColor. 118 | b nodes: sorted. 119 | b edges shape arrowedLine withShorterDistanceAttachPointWithJump. 120 | " head: RTEmptyNarrowArrow asHead;" 121 | b edges connectFromAll: #ancestors. 122 | b layout dominanceTree horizontalGap: 30 123 | "b layout sugiyama horizontalGap: 30 "]. 124 | b view @ RTZoomableView. 125 | ^ b 126 | ] 127 | 128 | { #category : #'visualizations - wrappers' } 129 | GitMigrationVisualization >> showProjectAncestryWithLabels [ 130 | ^ self showProjectAncestryWithLabels: true 131 | ] 132 | 133 | { #category : #'visualizations - wrappers' } 134 | GitMigrationVisualization >> showProjectAncestryWithLabels: aBoolean [ 135 | ^ self showProjectAncestryOn: migration versionsByPackage keys withLabels: aBoolean 136 | ] 137 | 138 | { #category : #colors } 139 | GitMigrationVisualization >> tailColor [ 140 | ^ Color cyan 141 | ] 142 | -------------------------------------------------------------------------------- /repository/GitMigration/TonelWriteError.class.st: -------------------------------------------------------------------------------- 1 | " 2 | I'm a writing error. 3 | I happen whenever an unrecoverable problem was encountered during writing of tonel. 4 | " 5 | Class { 6 | #name : #TonelWriteError, 7 | #superclass : #Error, 8 | #category : #GitMigration 9 | } 10 | -------------------------------------------------------------------------------- /repository/GitMigration/TonelWriter.extension.st: -------------------------------------------------------------------------------- 1 | Extension { #name : #TonelWriter } 2 | 3 | { #category : #'*GitMigration' } 4 | TonelWriter >> splitMethodSource: aMethodDefinition into: aBlock [ 5 | | keywords source declaration | 6 | 7 | keywords := aMethodDefinition selector keywords. 8 | source := aMethodDefinition source readStream. 9 | "Skip spaces" 10 | (source peek isSeparator) ifTrue: [ self skipSeparators: source ]. 11 | "Skip comments" 12 | (source peek = $") ifTrue: [ self skipComment: source ]. 13 | "Parse declaration" 14 | declaration := String new writeStream. 15 | [ (self selectorIsComplete: keywords in: declaration originalContents) not 16 | or: [ ':+-/\*~<>=@,%|&?!' includes: declaration contents trimRight last ] ] 17 | whileTrue: [ 18 | "stop infinite loop if no match was found" 19 | source atEnd ifTrue: [ TonelWriteError signal: 'Cannot find selector in source for ', aMethodDefinition asString ]. 20 | "get separators" 21 | [ source atEnd not and: [ source peek isSeparator ] ] 22 | whileTrue: [ declaration nextPut: source next ]. 23 | "take next word" 24 | [ source atEnd not and: [ source peek isSeparator not ] ] 25 | whileTrue: [ declaration nextPut: source next ] ]. 26 | aBlock 27 | value: (declaration contents trimLeft withLineEndings: self newLine) 28 | value: (source upToEnd withLineEndings: self newLine) 29 | ] 30 | -------------------------------------------------------------------------------- /repository/GitMigration/package.st: -------------------------------------------------------------------------------- 1 | Package { #name : #GitMigration } 2 | --------------------------------------------------------------------------------