Iceberg model and inner workings
Details about the inner working of Iceberg to understand how it commits, merges and handles conflicts.
Iceberg contains multiple IceRepository
. They are stored inside a registry.
icebergRepository := IceRepository registry detect: [ :each | each name = 'gtoolkit' ].
A repository has a IceWorkingCopy
that manage the status of all code loaded from the repository. It keeps track of what is loaded into the image (packages, comits, etc).
workingCopy := icebergRepository workingCopy
The working copy maintains list of IcePackage
from the repo.
workingCopy packages
A working copy can also be in several states. The state is computed using IceWorkingCopy>>#workingCopyState
.
workingCopy workingCopyState
States are subclasses of IceWorkingCopyState
IceInMergeWorkingCopy
: Indicates that a merge is in progress
IceUnknownVersionWorkingCopy
: indicates that the reference commit is an unknown commit.
IceEmptyWorkingCopy
: indicates that the referece commit is a IceNoCommit
IceAttachedSingleVersionWorkingCopy
: indicates that the reference commit is a valid commit.
Overall a repository can be in several states:
- Unknown commit: The head commit of the repository is IceUnknownCommit
. This can be a repository created as a placeholder. A fetch could be required to load the actual repository
- Detached Working Copy: The head commit in the repository is not the same as the head commit inside the image
- Detached HEAD: The head of the repisitory is a commit instead of a branch. When cloning a repository, if the latest version is cloned, head is set to a branch. If a particular commit is cloned, then the head points to that commit, and Iceberg considers the repository detached.
- No Project Found: Missing setting for configuring the location of the code and its format
-Not loaded: No code loaded
- Uncommited changes with outgoing or incomming commits.
Changes are committed bases on IceDiff
. A diff contains the changes between two Iceberg commitish. This can be a diff between two commits, or a diff between the current working copy and a commit.
workingCopy diffToReferenceCommit
For the rest of the demo we define the source and target commitish:
sourceCommitish := workingCopy. targetCommitish := workingCopy referenceCommit
sourceCommitish := workingCopy. targetCommitish := workingCopy referenceCommit ancestors first
sourceCommitish := workingCopy referenceCommit. targetCommitish := workingCopy referenceCommit ancestors first
sourceCommitish diffTo: targetCommitish
To compute a diff: (from the class comment):
- Asking to the repository the list of changed files/packages between the two versions. These are obtained, for example, by the Monticello dirty flags and the list of modified files provided by Git.
- The first step in computing a diff is to detect the type of changes between the source and the destination. This is a high level change, subclassing IceChange
, indicating the type of entity that changed (package), and where the changed occured (git or image):
- IceImageChange
: a change coming from the image (in contrast to a change coming from git)
- IceGitChange
: a change coming from git (in contrast to a change coming from the image)
- IceProjectChange
: the fact that the project changed
- IceCypressPropertiesChange
changes := sourceCommitish changesTo: targetCommitish
- Based on these high-level changes, the diff calculates two trees of IceDefinition
. Those trees are represented as compositions of IceNode
. These definitions are the logical entities at the level of the code model.
When commiting a diff is made betwen the working copy and the reference commit. The same mechanism could be used to get a diff between the working copy and another commit.
One type of diff is between two commits. In this case Iceberg performs the diff at the file level.
Every file that changes is modeled as a IceGitChange
. The importer IceChangeImporter
has a dedicated #selector
that uses a dedicated IceGitChangeImporter
to create nodes with the appropriate definitions.
The IceGitChangeImporter
creates IceDirectoryDefinition
and IceFileDefinition
for all normal files, until it encounters a older that contains a package. In that case it creates a MCSnapshot
from the version of that package inside the commit and uses a IceMCPackageImporter
to create Iceberg definitions for the content of that package. These definitions are created using IceMCDefinitionImporter
.
To determine the list of IceGitChange
between two version Iceberg gets the tree of files in each commit and the diff between them using LibGit.
icebergRepository changedFilesBetween: sourceCommitish and: targetCommitish.
fromTree := (LGitCommit of: icebergRepository repositoryHandle fromHexString: sourceCommitish id) tree.
toTree := (LGitCommit of: icebergRepository repositoryHandle fromHexString: targetCommitish id) tree.
gitTreeDiff := fromTree diffTo: toTree.
gitTreeDiff files collect: [ :each | IceGitChange on: each ]
diff := IceDiff new sourceVersion: sourceCommitish; targetVersion: targetCommitish; yourself
leftTree := IceNode value: IceRootDefinition new. changes do: [ :aChange | aChange accept: (IceChangeImporter new version: sourceCommitish; diff: diff; parentNode: leftTree; yourself) ]. leftTree
rightTree := IceNode value: IceRootDefinition new. changes do: [ :change | change accept: (IceChangeImporter new version: targetCommitish; diff: diff; parentNode: rightTree; yourself) ]. rightTree
- Then, the two trees are diff'd (IceDiff>>#diff:with:
), and a tree of differences is obtained. This tree is also a composition of IceNode
s, but contains IceOperation
objects instead (additions, deletions and modifications).
mergedTree := diff mergedTreeOf: leftTree with: rightTree.
tree := mergedTree select: [ :operation | operation hasChanges ].
The main entry point for performing a commit is IceWorkingCopy>>#commitChanges:withMessage:force:
. This:
- writes changes to the in-image git index. Code is written to the index only when comitting, not when the user is typing the code or saving the image.
- performs a commit with the changes in the index
This both writes the actual code changes to the on-disk git index and goes the commit
fullDiff := IceDiff from: sourceCommitish to: targetCommitish
To perform a commit code changes are written by Iceberg directly to the in-image git index. In the image this is an instance of IceGitIndex
. This maintains a list of modified file paths and can write them to the git index
icebergRepository index
IceGitIndex>>#updateDiskWorkingCopy:
uses a IceGitWorkingCopyUpdateVisitor
to write the code changes to disk, without changing the in-image index.
icebergRepository index updateDiskWorkingCopy: fullDiff
IceIndex>>#updateIndex:
adds the changed locations to the in-image git index.
icebergRepository index updateIndex: fullDiff
After changes are written to disk and the in-image git index is updates a commit can be done. This is in the method IceRepository>>#commitIndexWithMessage:andParents:
newCommit := icebergRepository commitIndexWithMessage: 'Example commit' andParents: (workingCopy workingCopyState referenceCommits reject: [ :each | each isNoCommit ]).
First changes are written to disk using IceGitIndex>>#addToGitIndex
Second a new empty index is created and installed in the repository
Third the state of the working copy and of all packages is updated based on the new commit
The merge between two versions is implemented by IceMerge
. This computes a merge tree with the changes that should be applied during the merge. The tree contain as nodes IceNode
objects that have as values subclasses of IceOperationMerge
. There are only two such types of operations:
- IceConflictingOperation
: a conflict between two operations that can be solved by using #selector
and #selector
.
- IceNonConflictingOperation
: a non-conflict between two operations that can be solved automatically. The user can still override the automatic choice using #selectLeft and #selectRight.
otherBranch := icebergRepository branchNamed: 'release'.
mergeAction := IceMerge new repository: icebergRepository; mergeCommit: otherBranch commit; yourself
The commit in case of merge uses the same logic as a normal user commit. Also the method IceWorkingCopy>>#commitChanges:withMessage:force:
is used. The difference is that the first parameter is now an instance of IceMerge
instead of a IceDiff
. The commit logic can visit both normal IceOperation
and IceOperationMerge
(IceGitWorkingCopyUpdateVisitor
and IceIndexUpdateVisitor
)
Iceberg registers to system announcers in IceSystemEventListener>>#registerSystemAnnouncements
and triggers a IceRepositoryModified
event for the iceberg repository that should be updated.
To detect which repository should be updated, Iceberg traverses the repositories looking for one that contains the changed package.
If the package is loaded into the image, it is marked as dirty in IceWorkingCopy>>#notifyPackageModified: