<not part of the commit message>
The minimal regedit engine is so big that it's tempting to move it
from dll.c to a separate module
</not part of the commit message>
This is an implementation of TODO "Need to not hard-code the dll path"
Registration is implemented with a minimal regedit engine: an array of
registry values { path, name, value } is the data source. So, all
registry entries are hard-coded in dll.c.
msysGit (for PathToMsys) is searched in the following order:
- %PATH%
- $(TARGET)/..
- $(TARGET)/../..
- InstallLocation of uninstall info (machine first, then user).
TODO: verify that the found git.exe is really Git
TODO: search for InstallLocation according to the type of the current
installation (default or -i:machine)
---
Makefile | 22 +--
dll.c | 390 ++++++++++++++++++++++++++++++++++++++++++++++++++---
git_shell_ext.def | 1 +
install.reg.in | 58 --------
uninstall.reg | 27 ----
5 files changed, 382 insertions(+), 116 deletions(-)
delete mode 100644 install.reg.in
delete mode 100644 uninstall.reg
diff --git a/Makefile b/Makefile
index 4042879..6076285 100644
--- a/Makefile
+++ b/Makefile
@@ -23,23 +23,17 @@ factory.o: factory.h ext.h menu.h
menu.o: menu.h ext.h debug.h systeminfo.h
systeminfo.o: systeminfo.h
-inst%: inst%.reg all
- regsvr32 -s $(DLL_PATH)
- regedit -s $<
+install: all
+ regsvr32 -s -n -i:machine $(DLL_PATH)
-uninst%: uninst%.reg
- regsvr32 -u -s $(DLL_PATH)
- regedit -s $<
+uninstall: all
+ regsvr32 -u -s -n -i:machine $(DLL_PATH)
-install.reg: install.reg.in Makefile
- sed < $< > $@ \
- -e 's|@@MSYSGIT_PATH@@|$(MSYSGIT_PATH)|' \
- -e 's|@@DLL_PATH@@|$(DLL_PATH)|'
+install-user: all
+ regsvr32 -s $(DLL_PATH)
-%-user.reg: %.reg
- sed < $< > $@ \
- -e 's|HKEY_LOCAL_MACHINE\\|HKEY_CURRENT_USER\\|' \
- -e
's|HKEY_CLASSES_ROOT\\|HKEY_CURRENT_USER\\Software\\Classes\\|'
+uninstall-user: all
+ regsvr32 -u -s $(DLL_PATH)
clean:
-rm -f $(OBJECTS) $(TARGET)
diff --git a/dll.c b/dll.c
index bab9d9a..40f52ae 100644
--- a/dll.c
+++ b/dll.c
@@ -1,12 +1,129 @@
#include <shlobj.h>
+#include <stdio.h>
#include "dll.h"
#include "ext.h"
#include "factory.h"
+#include "systeminfo.h"
/*
* The following is just the necessary infrastructure for having a .dll
* which can be registered as a COM object.
*/
+static HINSTANCE hInst;
+
+static const char *get_module_filename() {
+ static char module_filename[MAX_PATH] = { '\0' };
+
+ if ('\0' == module_filename[0]) {
+ DWORD module_size;
+
+ module_size = GetModuleFileName(hInst, module_filename,
MAX_PATH);
+ if (0 == module_size)
+ return NULL;
+ }
+
+ return module_filename;
+}
+
+#define MAX_REGISTRY_PATH MAX_PATH
+
+/* as per "How to: Convert Between System::Guid and _GUID" */
+static const char *get_class_id()
+{
+ static char class_id[MAX_REGISTRY_PATH] = { '\0' };
+
+ if ('\0' == class_id[0]) {
+ GUID guid = CLSID_git_shell_ext;
+ sprintf (class_id,
+ "{%08x-%04x-%04x-%02x%02x-%02x%02x%02x%02x%02x%02x}",
+ guid.Data1, guid.Data2, guid.Data3,
+ guid.Data4[0], guid.Data4[1], guid.Data4[2],
guid.Data4[3],
+ guid.Data4[4], guid.Data4[5], guid.Data4[6],
guid.Data4[7]);
+ }
+
+ return class_id;
+}
+
+/*
+ * Tries to find msysGit in the following order:
+ * %PATH%
+ * .. and ../.. (relative to the module)
+ * as qgit (via InstallLocation of Git)
+ SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Git_is1\InstallLocation
+ */
+static char msysgit[MAX_PATH] = { '\0' };
+
+static BOOL find_msysgit_in_path()
+{
+ char *file; // file part of the path to git.exe
+ DWORD dwFound; // length of path to git.exe
+
+ dwFound = SearchPath(NULL, "git.exe", NULL, MAX_PATH, msysgit, &file);
+ if (0 == dwFound || // git.exe is not in the PATH or
+ dwFound > MAX_PATH) // well, it's weird, and we're unprepared
+ return FALSE;
+
+ *(file - 5) = '\0'; // git exe is in "\bin\" from what we really need
+ return TRUE;
+}
+
+static BOOL find_msysgit_relative(const char *path)
+{
+ char *c;
+
+ strcpy(msysgit, get_module_filename());
+ c = strrchr(msysgit, '\\');
+ c[1] = '\0';
+ strcat(msysgit, path);
+ strcat(msysgit, "\\bin\\git.exe");
+ if (INVALID_FILE_ATTRIBUTES == GetFileAttributes(msysgit)) {
+ msysgit[0] = '\0'; // restore the original result
+ return FALSE;
+ }
+
+ c[1] = '\0';
+ strcat(msysgit, path);
+ return TRUE;
+}
+
+static BOOL find_msysgit_uninstall(HKEY root)
+{
+ HKEY key;
+ HRESULT result;
+ DWORD valuelen = MAX_PATH;
+
+ result = RegOpenKeyEx(root,
+
"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\Git_is1",
+ 0, KEY_READ, &key);
+ if (ERROR_SUCCESS != result)
+ return FALSE;
+
+ result = RegQueryValue(key, "InstallLocation", (LPBYTE)msysgit,
&valuelen);
+ return ERROR_SUCCESS == result;
+}
+
+static const char *find_msysgit()
+{
+ if ('\0' == msysgit[0]) {
+ if (find_msysgit_in_path())
+ return msysgit;
+
+ // try .. from our own directory
+ if (find_msysgit_relative(".."))
+ return msysgit;
+
+ // try ../..
+ if (find_msysgit_relative("..\\.."))
+ return msysgit;
+
+ /* TODO: find an elegant way to pass the installation type
+ down here */
+ if (! find_msysgit_uninstall(HKEY_LOCAL_MACHINE))
+ find_msysgit_uninstall(HKEY_CURRENT_USER);
+ }
+
+ return msysgit;
+}
HRESULT PASCAL DllGetClassObject(REFCLSID obj_guid, REFIID factory_guid,
void **factory_handle)
@@ -25,33 +142,272 @@ HRESULT PASCAL DllCanUnloadNow(void)
return (object_count || lock_count) ? S_FALSE : S_OK;
}
-HRESULT PASCAL DllRegisterServer(void)
+BOOL WINAPI DllMain(HINSTANCE instance, DWORD reason, LPVOID reserved)
{
- char module[MAX_PATH];
- wchar_t module_name[MAX_PATH];
- ITypeLib *typelib = NULL;
+ hInst = instance;
- GetModuleFileName(NULL, module, MAX_PATH);
- MultiByteToWideChar(CP_ACP, 0, module, -1, module_name, MAX_PATH);
- if (LoadTypeLib(module_name, &typelib) == S_OK) {
- HRESULT result = RegisterTypeLib(typelib, module_name, NULL);
- typelib->lpVtbl->Release(typelib);
- return result;
+ if (reason == DLL_PROCESS_ATTACH) {
+ object_count = lock_count = 0;
+ DisableThreadLibraryCalls(instance);
}
- return 1;
+
+ return 1;
+}
+
+/* replaces a substring pattern with a string replacement within a string
+ the replacement occurs in-place, hence string must be large enough to
+ hold the result
+
+ the function does not handle recursive replacements, e.g.
+ strreplace ("foo", "bar", "another bar"); // unexpected results
+
+ always returns *string
+*/
+static char *strreplace(char *string,
+ const char *pattern,
+ const char *replacement)
+{
+ char *tail;
+ char *found = strstr(string, pattern);
+
+ while (found) {
+ tail = strdup(found + strlen(pattern));
+
+ *found = '\0';
+ strcat(string, replacement);
+ strcat(string, tail);
+
+ free(tail);
+
+ found = strstr(string, pattern);
+ }
+
+ return string;
+}
+
+/*
+ * The following is required for registration/unregistration of a DLL
+ * It's basically a simplified regedit engine that supports
+ * @@CLSID@@ pattern to be replaced with CLSID of the extension;
+ * @@PROGRAM_NAME@@ - program_name variable;
+ * @@PROGRAM_PATH@@ - path to the extension;
+ * @@MSYSGIT_PATH@@ - path to msysgit;
+ *
+ * It attempts to convert values to LONG to create REG_DWORD values
+ */
+
+typedef struct REG_VALUE {
+ char *path;
+ char *name;
+ char *value;
+} REG_VALUE;
+
+static const REG_VALUE registry_info[] = {
+ { "SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Shell
Extensions\\Approved", "@@CLSID@@", "@@PROGRAM_NAME@@" },
+ { "SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Shell
Extensions\\Approved\\@@CLSID@@", NULL, NULL },
+ { "SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Shell
Extensions\\Approved\\@@CLSID@@", NULL,"@@PROGRAM_NAME@@" },
+ { "SOFTWARE\\Classes\\CLSID\\@@CLSID@@", NULL, NULL },
+ { "SOFTWARE\\Classes\\CLSID\\@@CLSID@@", NULL, "@@PROGRAM_NAME@@" },
+ { "SOFTWARE\\Classes\\CLSID\\@@CLSID@@\\InProcServer32", NULL, NULL },
+ { "SOFTWARE\\Classes\\CLSID\\@@CLSID@@\\InProcServer32", NULL,
"@@PROGRAM_PATH@@"},
+ { "SOFTWARE\\Classes\\CLSID\\@@CLSID@@\\InProcServer32",
"ThreadingModel", "Apartment" },
+ {
"SOFTWARE\\Classes\\*\\shellex\\ContextMenuHandlers\\@@PROGRAM_NAME@@",
NULL, NULL },
+ {
"SOFTWARE\\Classes\\*\\shellex\\ContextMenuHandlers\\@@PROGRAM_NAME@@",
NULL, "@@CLSID@@" },
+ {
"SOFTWARE\\Classes\\Directory\\shellex\\ContextMenuHandlers\\@@PROGRAM_NAME@@",
NULL, NULL },
+ {
"SOFTWARE\\Classes\\Directory\\shellex\\ContextMenuHandlers\\@@PROGRAM_NAME@@",
NULL, "@@CLSID@@" },
+ {
"SOFTWARE\\Classes\\Directory\\Background\\shellex\\ContextMenuHandlers\\@@PROGRAM_NAME@@",
NULL, NULL },
+ {
"SOFTWARE\\Classes\\Directory\\Background\\shellex\\ContextMenuHandlers\\@@PROGRAM_NAME@@",
NULL, "@@CLSID@@" },
+ {
"SOFTWARE\\Classes\\Drive\\shellex\\ContextMenuHandlers\\@@PROGRAM_NAME@@",
NULL, NULL },
+ {
"SOFTWARE\\Classes\\Drive\\shellex\\ContextMenuHandlers\\@@PROGRAM_NAME@@",
NULL, "@@CLSID@@"},
+ {
"SOFTWARE\\Classes\\Folder\\shellex\\ContextMenuHandlers\\@@PROGRAM_NAME@@",
NULL, NULL },
+ {
"SOFTWARE\\Classes\\Folder\\shellex\\ContextMenuHandlers\\@@PROGRAM_NAME@@",
NULL, "@@CLSID@@" },
+ {
"SOFTWARE\\Classes\\InternetShortcut\\shellex\\ContextMenuHandlers\\@@PROGRAM_NAME@@",
NULL, NULL },
+ {
"SOFTWARE\\Classes\\InternetShortcut\\shellex\\ContextMenuHandlers\\@@PROGRAM_NAME@@",
NULL, "@@CLSID@@" },
+ { GIT_CHEETAH_REG_PATH, NULL, NULL },
+ { GIT_CHEETAH_REG_PATH, GIT_CHEETAH_REG_PATHTOMSYS, "@@MSYSGIT_PATH@@"
},
+ { NULL, NULL, NULL }
+};
+
+static const REG_VALUE debug_info[] = {
+ { "Software\\Microsoft\\Windows\\CurrentVersion\\Explorer",
"DesktopProcess", "1" },
+ {
"Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\AlwaysUnloadDll",
NULL, NULL },
+ { "Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Advanced",
"SeparateProcess", "1" },
+ { NULL, NULL, NULL }
+};
+
+/* supports @@PROGRAM_NAME@@, @@PROGRAM_PATH@@, @@CLSID@@ patterns */
+static char *get_registry_path(const char *src, char dst[MAX_REGISTRY_PATH])
+{
+ if (NULL == src)
+ return NULL;
+
+ strcpy(dst, src);
+ strreplace(dst, "@@PROGRAM_NAME@@", program_name);
+ strreplace(dst, "@@PROGRAM_PATH@@", get_module_filename());
+ strreplace(dst, "@@CLSID@@", get_class_id());
+ strreplace(dst, "@@MSYSGIT_PATH@@", find_msysgit());
+
+ return dst;
+}
+
+/* uses get_registry_path to replace patterns */
+static HRESULT create_reg_entries(const HKEY root, REG_VALUE const info[])
+{
+ HRESULT result;
+ int i;
+
+ for (i = 0; NULL != info[i].path; ++i) {
+ char path[MAX_REGISTRY_PATH];
+ char name[MAX_REGISTRY_PATH], *regname = NULL;
+ char value [MAX_REGISTRY_PATH], *regvalue = NULL;
+
+ HKEY key;
+ DWORD disp; // required for RegCreateKeyEx, otherwise unused
+
+ get_registry_path(info[i].path, path);
+ result = RegCreateKeyEx(root, path,
+ 0, NULL, REG_OPTION_NON_VOLATILE, KEY_WRITE, NULL,
+ &key, &disp);
+ if (ERROR_SUCCESS != result)
+ return (result);
+
+ regname = get_registry_path(info[i].name, name);
+ regvalue = get_registry_path(info[i].value, value);
+
+ // regname can legitimately be NULL, but if value is NULL,
+ // it's just a key
+ if (NULL != regvalue) {
+ char *endptr;
+ DWORD dwValue = strtoul(regvalue, &endptr, 10);
+ if (endptr && '\0' == *endptr)
+ result = RegSetValueEx(key, regname, 0,
REG_DWORD,
+ (LPBYTE)&dwValue, sizeof(dwValue));
+ else
+ result = RegSetValueEx(key, regname, 0, REG_SZ,
+ (LPBYTE)regvalue, (DWORD)strlen(regvalue));
+ }
+
+ RegCloseKey(key);
+ if (ERROR_SUCCESS != result)
+ return (result);
+ }
+
+ return ERROR_SUCCESS;
+}
+
+static inline HRESULT mask_reg_errors(HRESULT const result)
+{
+ switch (result) {
+ case ERROR_FILE_NOT_FOUND: return ERROR_SUCCESS;
+ }
+
+ return result;
+}
+
+static HRESULT delete_reg_entries(HKEY const root, REG_VALUE const info[])
+{
+ HRESULT result;
+ int i = 0;
+
+ // count items in the array
+ while (NULL != info[i++].path);
+ // at this point, i is the __count__, so
+ // make it an offset to the last element
+ --i;
+
+ // walk the array backwards (we're at the terminating triple-null)
+ do {
+ char path[MAX_REGISTRY_PATH];
+ HKEY key;
+
+ --i;
+
+ get_registry_path(info[i].path, path);
+
+ if (info[i].name || info[i].value) { // if it's a value
+ char name[MAX_REGISTRY_PATH], *regname = NULL;
+
+ result = mask_reg_errors(RegOpenKeyEx(root, path,
+
0, KEY_WRITE, &key));
+ if (ERROR_SUCCESS != result)
+ return result;
+
+ // because some of our errors are masked (e.g. not
found)
+ // don't work on this key if we could not open it
+ if (NULL == key)
+ continue;
+
+ regname = get_registry_path(info[i].name, name);
+ result = mask_reg_errors(RegDeleteValue(key, regname));
+
+ RegCloseKey(key);
+ } else // it's a key
+ result = mask_reg_errors(RegDeleteKey(root, path));
+
+ if (ERROR_SUCCESS != result)
+ return (result);
+ } while (i);
+
+ return ERROR_SUCCESS;
+}
+
+HRESULT PASCAL DllRegisterServer(void)
+{
+ return create_reg_entries (HKEY_CURRENT_USER, registry_info);
}
HRESULT PASCAL DllUnregisterServer(void)
{
- return S_OK;
+ return delete_reg_entries(HKEY_CURRENT_USER, registry_info);
}
-BOOL WINAPI DllMain(HINSTANCE instance, DWORD reason, LPVOID reserved)
+/* provide means to create/delete keys:
+ - described in Debugging with The Shell;
+ - machine-wide registration and debugging info
+
+ possible combinations of regsvr32 command line options:
+ -n (absent) (present)
+ -i:
+ (absent) user reg (invalid)
+ debug user reg+debug user debug
+ machine user+machine reg machine reg
+ machinedebug user+machine reg+debug machine reg+debug
+
+ Obviously missing option is "machine debug". To accomplish:
+ - execute "regsvr32 -n -i:machinedebug" and then
+ - regsvr32 -u -n -i:machine
+*/
+HRESULT PASCAL DllInstall(BOOL bInstall, LPCWSTR pszCmdLine)
{
- if (reason == DLL_PROCESS_ATTACH) {
- object_count = lock_count = 0;
- DisableThreadLibraryCalls(instance);
+ BOOL bMachine = NULL != wcsstr(pszCmdLine, L"machine");
+ BOOL bDebug = NULL != wcsstr(pszCmdLine, L"debug");
+ HRESULT result = ERROR_INVALID_COMMAND_LINE;
+
+ if (bInstall) {
+ if (bMachine) {
+ result = create_reg_entries(HKEY_LOCAL_MACHINE,
+ registry_info);
+ if (ERROR_SUCCESS == result && bDebug)
+ result = create_reg_entries(HKEY_LOCAL_MACHINE,
+ debug_info);
+
+ } else if (bDebug) {
+ result = create_reg_entries(HKEY_CURRENT_USER,
+ debug_info);
+ }
+ } else { // uninstall
+ if (bMachine) {
+ result = delete_reg_entries(HKEY_LOCAL_MACHINE,
+ registry_info);
+ if (ERROR_SUCCESS == result && bDebug)
+ result = delete_reg_entries(HKEY_LOCAL_MACHINE,
+ debug_info);
+
+ } else if (bDebug) {
+ result = delete_reg_entries(HKEY_CURRENT_USER,
+ debug_info);
+ }
}
- return 1;
+ return result;
}
diff --git a/git_shell_ext.def b/git_shell_ext.def
index 472c184..bea88da 100644
--- a/git_shell_ext.def
+++ b/git_shell_ext.def
@@ -5,3 +5,4 @@ DllRegisterServer PRIVATE
DllUnregisterServer PRIVATE
DllCanUnloadNow PRIVATE
DllGetClassObject PRIVATE
+DllInstall PRIVATE
diff --git a/install.reg.in b/install.reg.in
deleted file mode 100644
index 2eb8f13..0000000
--- a/install.reg.in
+++ /dev/null
@@ -1,58 +0,0 @@
-Windows Registry Editor Version 5.00
-
-; This registry file creates neccessary entries for installation.
-; **** If you change this file, keep uninstall.reg in sync! ****
-;
-; This file is slated for being generated and not hard-coded.
-
-[HKEY_LOCAL_MACHINE\SOFTWARE\Git-Cheetah]
-
-[HKEY_LOCAL_MACHINE\SOFTWARE\Git-Cheetah]
-"PathToMsys"="@@MSYSGIT_PATH@@"
-
-[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Shell
Extensions\Approved]
-"{ca586c80-7c84-4b88-8537-726724df6929}"="Git-Cheetah"
-
-[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Shell
Extensions\Approved\{ca586c80-7c84-4b88-8537-726724df6929}]
[EMAIL PROTECTED]"Git-Cheetah"
-
-[HKEY_CLASSES_ROOT\CLSID\{ca586c80-7c84-4b88-8537-726724df6929}]
-
-[HKEY_CLASSES_ROOT\CLSID\{ca586c80-7c84-4b88-8537-726724df6929}]
[EMAIL PROTECTED]"Git-Cheetah"
-
-[HKEY_CLASSES_ROOT\CLSID\{ca586c80-7c84-4b88-8537-726724df6929}\InProcServer32]
[EMAIL PROTECTED]"@@DLL_PATH@@"
-
-[HKEY_CLASSES_ROOT\CLSID\{ca586c80-7c84-4b88-8537-726724df6929}\InProcServer32]
-"ThreadingModel"="Apartment"
-
-[HKEY_CLASSES_ROOT\*\shellex\ContextMenuHandlers\Git-Cheetah]
-
-[HKEY_CLASSES_ROOT\*\shellex\ContextMenuHandlers\Git-Cheetah]
[EMAIL PROTECTED]"{ca586c80-7c84-4b88-8537-726724df6929}"
-
-[HKEY_CLASSES_ROOT\Directory\shellex\ContextMenuHandlers\Git-Cheetah]
-
-[HKEY_CLASSES_ROOT\Directory\shellex\ContextMenuHandlers\Git-Cheetah]
[EMAIL PROTECTED]"{ca586c80-7c84-4b88-8537-726724df6929}"
-
-[HKEY_CLASSES_ROOT\Directory\Background\shellex\ContextMenuHandlers\Git-Cheetah]
-
-[HKEY_CLASSES_ROOT\Directory\Background\shellex\ContextMenuHandlers\Git-Cheetah]
[EMAIL PROTECTED]"{ca586c80-7c84-4b88-8537-726724df6929}"
-
-[HKEY_CLASSES_ROOT\Drive\shellex\ContextMenuHandlers\Git-Cheetah]
-
-[HKEY_CLASSES_ROOT\Drive\shellex\ContextMenuHandlers\Git-Cheetah]
[EMAIL PROTECTED]"{ca586c80-7c84-4b88-8537-726724df6929}"
-
-[HKEY_CLASSES_ROOT\Folder\shellex\ContextMenuHandlers\Git-Cheetah]
-
-[HKEY_CLASSES_ROOT\Folder\shellex\ContextMenuHandlers\Git-Cheetah]
[EMAIL PROTECTED]"{ca586c80-7c84-4b88-8537-726724df6929}"
-
-[HKEY_CLASSES_ROOT\InternetShortcut\shellex\ContextMenuHandlers\Git-Cheetah]
-
-[HKEY_CLASSES_ROOT\InternetShortcut\shellex\ContextMenuHandlers\Git-Cheetah]
[EMAIL PROTECTED]"{ca586c80-7c84-4b88-8537-726724df6929}"
diff --git a/uninstall.reg b/uninstall.reg
deleted file mode 100644
index bd33685..0000000
--- a/uninstall.reg
+++ /dev/null
@@ -1,27 +0,0 @@
-Windows Registry Editor Version 5.00
-
-; This registry file creates neccessary entries for uninstallation.
-; **** If you change this file, keep install.reg in sync! ****
-;
-; This file is slated for being generated and not hard-coded.
-
-[-HKEY_LOCAL_MACHINE\SOFTWARE\Git-Cheetah]
-
-[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Shell
Extensions\Approved]
-"{ca586c80-7c84-4b88-8537-726724df6929}"=-
-
-[-HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Shell
Extensions\Approved\{ca586c80-7c84-4b88-8537-726724df6929}]
-
-[-HKEY_CLASSES_ROOT\CLSID\{ca586c80-7c84-4b88-8537-726724df6929}]
-
-[-HKEY_CLASSES_ROOT\*\shellex\ContextMenuHandlers\Git-Cheetah]
-
-[-HKEY_CLASSES_ROOT\Directory\shellex\ContextMenuHandlers\Git-Cheetah]
-
-[-HKEY_CLASSES_ROOT\Directory\Background\shellex\ContextMenuHandlers\Git-Cheetah]
-
-[-HKEY_CLASSES_ROOT\Drive\shellex\ContextMenuHandlers\Git-Cheetah]
-
-[-HKEY_CLASSES_ROOT\Folder\shellex\ContextMenuHandlers\Git-Cheetah]
-
-[-HKEY_CLASSES_ROOT\InternetShortcut\shellex\ContextMenuHandlers\Git-Cheetah]
--
1.5.4.rc0.929.g50e2