換用TFS版控時我們開始採用「多重簽出」原則,大幅改善VSS時代「專案一被人簽出其他人就動不了」的困擾。但隨之而來的副作用是:多人同時修改,若簽入時別人已先簽入更新的版本,就需要執行程式碼合併。
在我們的經驗裡,TFS有個神奇又方便的「自動合併」功能,只要程式修改幅度不大,沒有改到同一段程式,TFS幾乎都能正確自動合併,不需人為介入,少數難以判別的情況才會跳出提示要求人工處理。
但時間久了,我不免懷疑,程式碼合併的情境百百種,肉眼合併都難保沒有疏漏,演算法要怎麼寫才能不出錯?但這一兩年下來,記憶中都還真沒出過亂子,TFS自動合併演算法直逼AlphaGo呀,令人讚嘆不已… 直到我膝蓋中了一箭!
前幾天在合併分支時出現一起TypeScript屬性重複案例,追查發現是自動合併造成的。有一段CodeGen產生的TypeScript Model定義,因故調換屬性順序,自動合併時調換位置的屬性出現兩次釀禍。
用實例說明比較好理解,注意下例TypeScript型別的SomePrz及SomeQty屬性,原本排在ColB之後,ColC之前:
export class Blah extends ViewModelBase {
/** 欄位A */
ColA: KnockoutObservable<any> = ko.observable();
/** 欄位B */
ColB: KnockoutObservable<any> = ko.observable();
//...省略...
/** 價格 */
SomePrz: KnockoutObservable<any> = ko.observable();
/** 數量 */
SomeQty: KnockoutObservable<any> = ko.observable();
/** 欄位C */
ColC: KnockoutObservable<any> = ko.observable();
/** 欄位D */
ColD: KnockoutObservable<any> = ko.observable();
/** 欄位E */
ColE: KnockoutObservable<any> = ko.observable();
/** 欄位F */
ColF: KnockoutObservable<any> = ko.observable();
/** 欄位G */
ColG: KnockoutObservable<any> = ko.observable();
/** 欄位H */
ColH: KnockoutObservable<any> = ko.observable();
//...省略...
}
Branch版本裡類別增加了幾個額外屬性,並調動了SomePrz及SomeQty順序,由ColC前方移至ColE後方:
export class Blah extends ViewModelBase {
/** 欄位A */
ColA: KnockoutObservable<any> = ko.observable();
/** 欄位B */
ColB: KnockoutObservable<any> = ko.observable();
//...省略...
/** 欄位C */
ColC: KnockoutObservable<any> = ko.observable();
/** 欄位D */
ColD: KnockoutObservable<any> = ko.observable();
/** 欄位E */
ColE: KnockoutObservable<any> = ko.observable();
/** 價格 */
SomePrz: KnockoutObservable<any> = ko.observable();
/** 數量 */
SomeQty: KnockoutObservable<any> = ko.observable();
/** 欄位F */
ColF: KnockoutObservable<any> = ko.observable();
/** 欄位G */
ColG: KnockoutObservable<any> = ko.observable();
/** 欄位H */
ColH: KnockoutObservable<any> = ko.observable();
//...省略...
}
Merge回主線時,由TFS自動合併解決衝突,變成以下的樣子,SomePrz/SomeQty在ColC欄位前方及ColE後方各出現一次。
export class Blah extends ViewModelBase {
/** 欄位A */
ColA: KnockoutObservable<any> = ko.observable();
/** 欄位B */
ColB: KnockoutObservable<any> = ko.observable();
//...省略...
/** 價格 */
SomePrz: KnockoutObservable<any> = ko.observable();
/** 數量 */
SomeQty: KnockoutObservable<any> = ko.observable();
/** 欄位C */
ColC: KnockoutObservable<any> = ko.observable();
/** 欄位D */
ColD: KnockoutObservable<any> = ko.observable();
/** 欄位E */
ColE: KnockoutObservable<any> = ko.observable();
/** 價格 */
SomePrz: KnockoutObservable<any> = ko.observable();
/** 數量 */
SomeQty: KnockoutObservable<any> = ko.observable();
/** 欄位F */
ColF: KnockoutObservable<any> = ko.observable();
/** 欄位G */
ColG: KnockoutObservable<any> = ko.observable();
/** 欄位H */
ColH: KnockoutObservable<any> = ko.observable();
//...省略...
}
所幸,TypeScript為強型別,不允許屬性重複宣告,我們很快查出錯誤,合併Branch時暫時停用自動合併功能即可避開問題。
出問題的TypeScript檔案不小,有近2500行,而重複屬性的位置在1668行左右。事後我試著模擬屬性順序調動的情境,卻怎麼都無法重現自動合併錯誤,猜想需要夠大的檔案或某些特殊條件下才會導致自動合併誤判。
爬文沒有找到太多TFS自動合併錯誤的個案,有一個接近的案例是自動合併.csproj發生項目重複,原因也跟項目順序重排有關,證明調換順序是差異比對演算法的大魔王無誤!
這是我第一次遇到TFS自動合併失誤,說不上震憾,充其量只是證實「不存在完美的差異比對演算法」的假設,不致影響我對它的信心。就當成一記失投,它依然是我心目中的賽揚獎投手~ 經此經驗,未來使用自動合併時會提高警覺,預做它可能出錯的心理準備。
當然,如果你寧可辛苦一點也不想承擔任何出錯風險,可考慮將它關閉(如下圖),大家請自行拿捏。