VBA編輯器上的版本控制

前言

在撰寫程式碼時,許多人習慣將程式與檔案綁定,並利用文件名稱來標記每次版本的修改。例如,將當天日期加在檔案名稱後來區隔版本,如 縱斷面繪圖20230622.xls。這是我過去對版本控制的理解,然而,這樣做的問題是檔案數量快速增長,且檔案名稱無法清楚標示每次修改的具體內容。

更糟的是,當這些檔案分散於不同的電腦和資料夾中,若同名的檔案有所不同,卻無法得知具體改動了哪些部分。這樣的管理方式會造成很大的困擾,也促使我開始尋找更有效的版本控制方法。

因此,我開始探索如何在 VBA 中實現「真」版本控制,並結合 Git 來管理程式碼。

更新註記 (2024/05/19)

當我將程式上傳至 GitHub 時,發現因為筆記本的預設編碼為 "ANSI",上傳的中文部分會顯示為亂碼。為了解決這個問題,我將編碼另存為 "UTF-8",這樣可以正常顯示。但是,若將 "UTF-8" 編碼匯入 Excel VBA IDE 時,由於 Excel 預設支援 ANSI 編碼,中文會出現錯誤。

編輯器開發環境

在撰寫 VBA 程式碼時,主要會有以下幾個區域來放置程式碼:

  • Excel 物件(如 ThisWorkbook, Worksheet)
  • 模組(Module)
  • 物件類別模組(Class)
  • 表單(Form)

在這篇文章中,我假設程式碼並未直接寫在 Worksheet 中,因為這樣的程式碼會帶來許多匯入上的麻煩。而 ThisWorkbook 雖然屬於 doccls 類型的一部分,但其中可能會有需要手動處理的特殊情況,開發者需根據需要自行處理這些內容。

GIT 基本操作

對於 GIT 的基本操作,網路上有許多資源可以參考。感謝六角學院的Youtube教學

常見的 GIT 操作如下:

  • git init:初始化資料夾
  • git add:將檔案加入索引
  • git commit:提交版本註記
  • git push:將程式碼上傳至 GitHub
  • git clone:從 GitHub 下載程式碼
  • git fetch:從 GitHub 下載並更新至本地資料夾
  • git checkout:切換分支或版本

操作步驟

  1. 啟用 VBA 設定:在 Excel 中,打開「開發者」選單,並選擇「信任存取 VBA 專案物件模型」。

  2. 初始化 Git:打開 Git Bash,指定一個資料夾並執行 git init

  3. 匯入 Git 模組:在目前的 VBA IDE 中匯入 GIT.bas 模組。

  4. 導出程式碼:執行巨集 ExportCodesToFolder,並選擇第二步中指定的資料夾。

  5. 確認版本:若確定無誤,繼續使用 Git 進行版本控制。若有任何問題,回到第 4 步重新檢查。

  6. 進行 Git 操作:使用 Git Bash 進行 git addgit commitgit push 等操作。

處理程式碼版本問題

若發現開啟程式碼後發現有問題,可以執行以下步驟:

  1. 在 VBA IDE 中匯入 GIT 模組。
  2. 執行 DeleteCodes 來刪除舊有的程式碼。
  3. 執行 ImportCodes,並選擇指定的資料夾來匯入最新版本的程式碼。

VBA 程式碼範例

以下是使用 Git 控制 VBA 程式碼版本的核心程式碼範例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230

'TODO:Export folder need to be killed

Sub ExportCodesToFolder()

'Type: 1=bas,2=cls,3=frm

myFolder = getSavedFolder

Call killFilesInFolder(myFolder)

Set VBProj = ThisWorkbook.VBProject
For Each VBComp In VBProj.VBComponents

Select Case VBComp.Type

Case 1: myExtension = ".bas"
Case 2: myExtension = ".cls"
Case 3: myExtension = ".frm"

Case 100: myExtension = ".doccls"

End Select

full_path = myFolder & "\" & VBComp.Name & myExtension

If myExtension <> "" Then

VBComp.Export (full_path)

End If

If myExtension = ".doccls" And CountFileLines(full_path) = 9 Then Kill full_path

Next VBComp

End Sub

Sub killFilesInFolder(folderPath)

Set coll_path = GetFilePathsInFolder(folderPath)

For Each filePath In coll_path

Filename = mid(filePath, InStrRev(filePath, "\") + 1)
fileExtension = mid(Filename, InStrRev(Filename, ".") + 1)

If fileExtension = "frm" Or fileExtension = "bas" Or fileExtension = "cls" Or fileExtension = "doccls" Then
Kill filePath
End If
Next

End Sub

Sub ImportCodes()

myFolder = getSavedFolder

Set coll_path = GetFilePathsInFolder(myFolder)

Call DeleteCodes

For Each filePath In coll_path

Filename = mid(filePath, InStrRev(filePath, "\") + 1)
fileExtension = mid(Filename, InStrRev(Filename, ".") + 1)

If fileExtension = "frm" Or fileExtension = "bas" Or fileExtension = "cls" Then
Call ImportCode(filePath, Filename)
End If

Next

End Sub

Sub ImportCode(ByVal filePath As String, ByVal Filename As String)

extension = mid(Filename, InStrRev(Filename, ".") + 1)
CodeName = mid(Filename, 1, InStrRev(Filename, ".") - 1)

If CodeName = "GIT" Then Exit Sub

Set VBProj = ThisWorkbook.VBProject

'If checkIfCodeExist(CodeName) = True Then
'
' Set vbcomp = VBProj.VBComponents(CodeName)
' VBProj.VBComponents.Remove (vbcomp)
'
'End If

VBProj.VBComponents.Import (filePath)

End Sub

Sub DeleteCodes()

'Type: 1=bas,2=cls,3=frm

Set VBProj = ThisWorkbook.VBProject
For Each VBComp In VBProj.VBComponents

Select Case VBComp.Type

Case 1: myExtension = ".bas"
Case 2: myExtension = ".cls"
Case 3: myExtension = ".frm"

Case 100: myExtension = ".doccls"

End Select

If VBComp.Type <> 100 And VBComp.Name <> "GIT" Then

VBProj.VBComponents.Remove (VBComp)

End If

Next VBComp

End Sub

'--------FUNCTION------------

Function GetFilePathsInFolder(ByVal folderPath As String)

Dim coll As New Collection

Dim fso As Object
'Dim folderPath As String
Dim folder As Object
Dim file As Object

Set fso = CreateObject("Scripting.FileSystemObject")

' folderPath = getSavedFolder
Set folder = fso.GetFolder(folderPath)

For Each file In folder.Files

coll.Add file.Path

Next file

Set file = Nothing
Set folder = Nothing
Set fso = Nothing

Set GetFilePathsInFolder = coll

End Function

Function getSavedFolder()

Set fldr = Application.FileDialog(4)

With fldr
.Title = "Select a Folder"
.AllowMultiSelect = False
.InitialFileName = ThisWorkbook.Path
If .Show = -1 Then FolderName = .SelectedItems(1)
End With
getSavedFolder = FolderName

End Function

Function checkIfCodeExist(ByVal checkName As String) 'useless

Set VBProj = ThisWorkbook.VBProject
Set VBComps = VBProj.VBComponents

checkIfCodeExist = False

For Each it In VBComps

If it.Name = checkName Then

checkIfCodeExist = True: Exit Function

End If
Next

End Function

Function CountFileLines(ByVal filePath)

Dim FileContent As String
Dim fileNumber As Integer
Dim lineCount As Long

' Open the text file
fileNumber = FreeFile
Open filePath For Input As fileNumber

' Read the file content line by line and count the lines
Do Until EOF(fileNumber)
Line Input #fileNumber, FileContent
lineCount = lineCount + 1
Loop

' Close the file
Close fileNumber

' Display the line count in cell A1
CountFileLines = lineCount

End Function

'--------TMP_CODE-------------

Function tmp_deleteCodes()

Set VBProj = ThisWorkbook.VBProject
Set VBComps = VBProj.VBComponents

For Each it In VBComps

If it.Name Like "*2" And it.Type <> 100 Then

CodeName = it.Name

Set VBComp = VBProj.VBComponents(CodeName)
VBProj.VBComponents.Remove (VBComp)

End If

Next

End Function