每次系統上線時,完整保留建置當時使用的原始碼版本是版本控制的基本守則,跟燙傷時要沖脫泡蓋送一樣屬於生活須知。落實了這一步,才不會在後續開發產生分支後(例如: 修改部分程式編譯成另一套版本與原版本並存運行),落入找不齊該版本原始碼,程式從此無法修改的悲慘下場。

針對這類需求,版控軟體多已設想周到,提供了如分支(Branch)、標籤(Label)等功能協助管理版本(連清朝就出現的版控軟體VSS都有)。

不過,人難免年少荒唐,即便有VSS版控,手邊卻還是有個專案因為時程趕外加偷懶茍且又無知,草草Build完上線就繼續改寫加入新功能,等後來因程式規格差異決定要拆出另一個特別版本與原版本並行時,才發現VSS中部分程式已參雜了上線後才修改簽入的部分(而且還改很大),再也找不齊當初編譯所用的版本,搞出一個再也無法修改重新編譯的程式怪獸...

最近,抱著贖罪的心情,決定開發一個小工具程式,可在VSS中搜羅當初編譯之前所簽入的最後版本號碼,如此即可整理出編譯當時"最新的程式碼",找齊當初編譯所用的原始碼。(前題是平日有確實遵守"編譯前必須先Check In"準則,這相當於"上完廁所要洗手"的生活須知等級,若不幸遇上衛生習慣如此不堪的開發者,那... 節哀吧!)

開始研究才發現,VSS老歸老,在自動化整合方面可不含糊,微軟甚至也準備好Microsoft.VisualStudio.SourceSafe.Interop,只要在.NET專案中加入參考,就可運用程式存取VSS資料庫,取得檔案、版本資訊,甚至進行簽入、簽出等各項版控操作

於是我寫了以下的工具:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml.Linq;
using Microsoft.VisualStudio.SourceSafe.Interop;
using System.IO;
 
namespace DumpVssProjectStruc
{
    public class VssReader
    {
        IVSSDatabase vssdb = new VSSDatabase();
        DateTime? notAfter = null;
        public VssReader(string ssdir, string ssuser, string sspwd)
        {
            vssdb.Open(ssdir, ssuser, sspwd);
        }
        //取得專案檔案結構XML,傳入saveFolder參數可將最後版本檔案存入指定目錄
        //傳入notAfter則可選取某個時間之前簽入的檔案
        public XDocument ReadProject(string path, 
            string saveFolder = null, DateTime? notAfter = null)
        {
            this.notAfter = notAfter;
            XDocument xd = XDocument.Parse("<P />");
            Explore(path, xd.Root, saveFolder);
            return xd;
        }
        //以遞迴方式取得專案結構的所有檔案,若傳入savePath則會一併取得檔案寫入
        public void Explore(string path, XElement container, string savePath = null)
        {
            VSSItem proj = vssdb.get_VSSItem(path, false);
            //在XML建立目錄元素
            XElement dir = new XElement("D", 
                new XAttribute("P", proj.Spec), new XAttribute("N", proj.Name));
            if (!string.IsNullOrEmpty(savePath))
            {
                //若有指定儲存位置,計算路徑並確保資料夾已建立
                savePath = Path.Combine(savePath, proj.Name);
                if (!Directory.Exists(savePath))
                    Directory.CreateDirectory(savePath);
            }
            foreach (VSSItem item in proj.get_Items(false))
            {
                //若為VSSITEM_PROJECT,等同目錄,要再展開其下的子項目
                if (item.Type == (int)VSSItemType.VSSITEM_PROJECT)
                    Explore(item.Spec, dir, savePath);
                else
                {
                    //在XML建立檔案項目元素
                    XElement file = new XElement("F",
                        new XAttribute("P", item.Spec), 
                        new XAttribute("N", item.Name));
                    bool saved = false;
                    //取得該項目的所有版本資訊
                    foreach (VSSVersion v in item.Versions)
                    {
                        //若有指定簽入時間限制,忽略晚於限制時間的版本
                        if (notAfter != null & v.Date.CompareTo(notAfter) > 0)
                            continue;
                        //在檔案項目元素下新增版本元素
                        file.Add(new XElement("V",
                                new XAttribute("N", //若為標籤,則在前方加上l當作版號
                                    string.IsNullOrEmpty(v.Label) ? 
                                    v.VersionNumber.ToString() : "l" + v.Label),
                                new XAttribute("U", v.Username),
                                new XAttribute("T", 
                                    v.Date.ToString("yyyy/MM/dd HH:mm:ss"))
                            ));
                        //存入第一個吻合的版本(忽略標籤版本)
                        if (!string.IsNullOrEmpty(savePath) &&
                            string.IsNullOrEmpty(v.Label) && !saved)
                        {
                            string filePath = Path.Combine(savePath, item.Name);
                            v.VSSItem.Get(ref filePath, 0);
                            saved = true;
                        }
                        //若有簽入時間限制,則只包含吻合的第一筆
                        if (notAfter != null) break;
                    }
                    dir.Add(file);
                }
            }
            container.Add(dir);
        }
    }
}

先執行VssReader vr =  new VssReader("\\ vss_server\vssShare", ssUser, ssPwd); 可開啟指定的VSS資料庫。

接著Console.Write(vr.ReadProject("$/TBM/AuditLogon").ToString());即可取得如下包含版本歷程的VSS專案結構XML檔: (這個產出結果也可用來產生報表或進行其他分析,挺好用的)

<P>
  <D P="$/TBM/AuditLogon" N="AuditLogon">
    <F P="$/TBM/AuditLogon/AssemblyInfo.cs" N="AssemblyInfo.cs">
      <V N="lSealed" U="jeffrey" T="2011/11/23 17:33:26" />
      <V N="1" U="jeffrey" T="2008/07/03 12:14:28" />
    </F>
    <F P="$/TBM/AuditLogon/AuditLogon.csproj" N="AuditLogon.csproj">
      <V N="lSealed" U="jeffrey" T="2011/11/23 17:33:26" />
      <V N="1" U="jeffrey" T="2008/07/03 12:14:28" />
    </F>
    <F P="$/TBM/AuditLogon/Form1.cs" N="Form1.cs">
      <V N="3" U="jeffrey" T="2011/11/25 16:02:15" />
      <V N="lSealed" U="jeffrey" T="2011/11/23 17:33:26" />
      <V N="2" U="jeffrey" T="2008/07/15 14:25:32" />
      <V N="1" U="jeffrey" T="2008/07/03 12:14:28" />
    </F>
    <F P="$/TBM/AuditLogon/Form1.resx" N="Form1.resx">
      <V N="lSealed" U="jeffrey" T="2011/11/23 17:33:26" />
      <V N="1" U="jeffrey" T="2008/07/03 12:14:28" />
    </F>
  </D>
</P>

如果要取得一份2010/04/01 16:00:00前簽入的原始檔案集合並儲存於D:\\SourceRecovery,執行
vr.ReadProject("$/TBM/AuditLogon", @"D:\SourceRecovery", new DateTime(2010, 4, 1, 16, 0, 0));

改不動的系統終於重見天日,這下可以重新做人囉~~


Comments

# by Alex Lee

"跟燙傷時要沖脫泡蓋送一樣樣屬於生活須知。" "樣"字重複~

# by Jeffrey

to Alex, 已修正,感謝!

Post a comment