Colin Putney wrote:
On Feb 26, 2005, at 11:20 PM, Florin Mateoc wrote:
We have two versions of a method, both with complete version history. Because we have the version history, it doesn't really matter that the two versions come from different packages, it's exactly the same as merging two versions of the same package. So instead of one version overriding the other, we do a merge. By comparing the method histories we can decide if one version supersedes the other. That would mean that it's an updated version of the other, which means we can rely on the user's wisdom again. If the user changed the method from one of the versions we have to the other one, he must know what he's doing. Therefore we use which ever version the user has already chosen.
I am sorry, but this is simply not true. A developer may choose, in a newer version of a class, to ignore some unrelated development, and stick to an older protocol, by including some older versions for some of the methods. This is not a made up example, I have encountered the situation quite often. You can easily have, as a simplistic example, PackageA>ClassB>methodC(version1),methodD(version2) and PackageE>ClassB>methodC(version2),methodD(version1). The automatic resolution will do the wrong thing, and it won't even inform the user.
Ok, let's get into this in excruciating detail, because it's not clear to me why you think the above example cannot be resolved correctly. Let's say I have the following program elements, drawn from your example, with history.
PackageA
ClassB>>methodC.version1 (ancestors: version0) ClassB>>methodD.version2 (ancestors: version0, version1) ClassB>>MethodE.version1 (ancestors: version0)
PackageZ
ClassB>>methodC.version2 (ancestors: version0, version1) ClassB>>methodD.version1 (ancestors: version0) ClassB>>methodE.version2 (ancestors: version0)
Ok, so let's see what happens if we load both packages into the same image. PackageA and PackageZ both define methodC, and they have different definitions. So we've got to decide which version, if any, will be in the image. The version in PackageA is called version1, and it was derived from version0. The version in PackageZ is called version2, and lists version1 as its ancestor. Therefore, version2 was created by modifying version1. So we'll choose version2, from PackageZ.
MethodD has the reverse situation. PackageA's version is a descendent of PackageZ's, so we'll choose version2 again, but this time from PackageA.
MethodE presents a conflict. Both versions descend from a common ancestor, but neither descends from the other. So we pop up a conflict resolution window, and let the user decide what methodE should look like when both packages are present. This results in a new version, called version3. When we're done, the image looks like this:
ClassB>>methodC.version2 (ancestors: version0, version1) ClassB>>methodD.version2 (ancestors: version0, version1) ClassB>>methodE.version3 (ancestors: version0, version1, version2)
Now, you are correct to point out that, say, methodC.version2 might have been developed in PackageZ without PackageA loaded, and so might not work as PackageA expects. Perhaps we should indeed log to the Transcript when a merge automatically resolves overlapping packages. There is no *guarentee* that version2 will work right. But our chances of success are better if we follow the intention of the developer of version2, which was to replace version1. Following the order that packages are loaded is little different than choosing at random.
But there is no way to divine the intentions of a developer from the version history. When a developer intentionally chooses to include an older version, his or her intention is to have that older version loaded, and not the newer one, regardless of their ancestry. You cannot say that you follow any developer's intention by automatically choosing a newer version instead of an older one. There is such a thing as intentionally reverting to an older version.
In our particular case, the only intention known for the developer of PackageA is that methodC.version1 should work with methodD.version2 and methodE.version1. For the developer of PackageZ we know that methodC.version2 should work with methodD.version1 and methodE.version2. There is no manifest developer who ever had the intention to load methodC.version2 together with methodD.version2, let alone having tested this combination. There is a big chance that because of the automatic merge in such a case, none of the two packages will work anymore. Quite often, in the same repository you have multiple streams of development. A new fix for an old, production image, may mean backporting from the current, development image. Some of the new methods are appropriate, some not. By letting the last package "win", not only do you not choose at random, but you get pretty close to guaranteeing that at least the last package will function. For me, at least in programming, where I like my universe deterministic, a "guarantee" for less is better than a hope for more.
Making independently developed packages work together means (intelligent) work, and if there's any overlap, the chances of solving the issues automatically are, I believe, very slim, and versioning does not help. Even if all the common methods in one of the packages are newer versions (and descendants) of the same methods in the other packages, it still doesn't mean that they are made to work with the older package, it may simply mean that the newer package is supposed to work with a newer version of the older package. I think the only situation where you can say that there is no conflict is when the common methods are all identical, and for this you don't need versions. This is why, to my mind, overrides have nothing to do with versioning, they are simply a different kind of extension.
I agree that it takes intelligent work to make packages work together, and I'm not suggesting that the computer can do that. I am suggesting that, having done the work, we record the results so that we don't have to do it again everytime we load those packages.
[snip]
This is probably just the memory of a frustration with Envy: because it stores all these method editions (inluding every time you put a "halt" in a method), the noise level is pretty high, so I always wished that I could see at a glance, when looking at the list of editions for a method, which editions are "real". But even if we have explicit method versioning, so the noise is reduced, the most "real" ones are the ones associated with the holder's version, because there is an implicit minimal testing expectation for versions.For the method version I would expect something like a unit-test, for a class, the beginning of some functional testing. The expectation is even higher for the package, because it usually groups together classes working in tight coupling, so the testing done for a package version is more of a functional test, so now those methods "really" work. I guess it would be fun to disallow versioning if we detect that testing was not performed :) Seriously though, it might be interesting if we could link somehow versions to the tests performed.
Ok, I see. You just want to define a group of program elements that should be versioned together. With Monticello you do this explicitly, so there's a lot less noise. Everything is a "real" version, and they correspond to a bunch of other "real" versions that were current at the same time.
Even if you do it explicitely, not all versioning happens at the same time. I develop a method, it looks good, I test it a little (workspace, unit-test, whatever), I am happy with it and I want to keep it. I version it (separately, because this is what method-level granularity means).
Interesting, because that's not what I mean by "method-level granularity."
In MC1 (and Store, as far as I can tell), only packages have ancestry. The ancestry of a method has to be reconstructed by examing all the versions of the package it appears in and noting how it changes. In MC2 (and Envy, as far as I can tell), methods have individually recorded ancestry. This is why I say that MC2 versions at method-level granularity.
But that doesn't mean that you have to version new methods in isolation, whether explicitly or with every accept as in Envy. If you do that, you loose the spacial context I mentioned in my last post. That version is just noise, so why bother? It's not like the method is going anywhere. You can save your image without versioning it, and even if you manage to crash the VM you can always pull it out of the change log.
Of course that you don't have to version the new method in isolation, but if you do want to, why not be able to do it? Who decides for me that something that I want to name and keep and reference later, share with others, is just noise, and I should not bother? In the course of developing something, the way the code progresses is meaningful, and you may want to maintain a history with these snapshots. This is a very cheap way of documenting what you are doing. Granted, the value of older versions decreases over time, but that is true for all versions, at every level, not just for methods. But I can tell you that every time I decided to purge something from arepository, to drop something from the history of a project, I came to regret it later.
In MC1 you always version whole packages at a time. MC2 is more flexible, in that you can specify other ways of separating the code you are interested in from the rest of the image. But whatever your method of segregation, you always version all of it at once. So in this sense, Monticello versions at "project-level granularity," the project being whatever you're working on, be it a package, change set or whatever. It's important to do that so as to get the spacial context needed to merge snapshots correctly.
Part of a "project" may mean reverting a method to an existing, older version, reverting a class to an older version, reverting a package to an older version. This happens quite frequently when developing a patch. What does it mean for these components' history that you re-version them?
Colin