Использование LibGit2Sharp для усечения истории коммитов GIT
Я планирую использовать LibGit2/LibGit2Sharp и, следовательно, GIT неортодоксальным способом, и я прошу любого, кто знаком с API, подтвердить, что то, что я предлагаю, теоретически будет работать.:)
сценарий
Только основная ветвь будет существовать в хранилище. Большое количество каталогов, содержащих большие двоичные и недвоичные файлы, будет отслеживаться и фиксироваться. Большинство бинарных файлов будет меняться между фиксациями. Репозиторий должен содержать не более 10 коммитов из-за ограничений дискового пространства (в настоящее время диск часто заполняется).
Что API не предоставляет, так это функция, которая усекает историю коммитов, начиная с указанного CommitId, до первоначального коммита главной ветви и удаляет любые объекты GIT, которые в результате будут зависать.
Я проверил, используя метод ReferenceCollection.RewiteHistory, и я могу использовать его, чтобы удалить родителей из коммита. Это создает мне новую историю коммитов, начинающуюся с CommitId и возвращающуюся в HEAD. Но это все же оставляет все старые коммиты и любые ссылки или капли, которые являются уникальными для этих коммитов. Мой план сейчас состоит в том, чтобы просто убрать эти свисающие объекты GIT самостоятельно. Кто-нибудь видит какие-либо проблемы с этим подходом или есть лучший?
2 ответа
Но это все же оставляет все старые коммиты и любые ссылки или капли, которые являются уникальными для этих коммитов. Мой план сейчас состоит в том, чтобы просто убрать эти свисающие объекты GIT самостоятельно.
Переписывая историю хранилища, LibGit2Sharp заботится о том, чтобы не выбросить переписанную ссылку. Пространство имен, в котором они хранятся, по умолчанию refs/original
, Это можно изменить через RewriteHistoryOptions
параметр.
Чтобы удалить старые коммиты, деревья и BLOB-объекты, сначала нужно удалить эти ссылки. Это может быть достигнуто с помощью следующего кода:
foreach (var reference in repo.Refs.FromGlob("refs/original/*"))
{
repo.Refs.Remove(reference);
}
Следующим шагом будет очистка теперь висящих объектов git. Однако это не может быть сделано через LibGit2Sharp (пока). Одним из вариантов будет выложить git следующую команду
git gc --aggressive
Это очень эффективным / разрушительным / невосстанавливаемым образом уменьшит размер вашего хранилища.
Кто-нибудь видит какие-либо проблемы с этим подходом или есть лучший?
Ваш подход выглядит обоснованным.
Обновить
Кто-нибудь видит какие-либо проблемы с этим подходом или есть лучший?
Если пределом является размер диска, другой вариант - использовать такой инструмент, как git-annex или git-bin, для хранения больших двоичных файлов вне репозитория git. Посмотрите этот SO вопрос, чтобы получить различные взгляды на предмет и потенциальные недостатки (развертывание, блокировка,...).
Я попробую RewriteHistoryOptions и foreach-код, который вы предоставили. Тем не менее, на данный момент это выглядит как File.Delete для висячих объектов git для меня.
Осторожно, это может быть ухабистая дорога
- Git хранит объекты в двух форматах. Свободный (один файл на диске на объект) или упакованный (одна запись на диске, содержащая много объектов). Удаление объектов из файла пакета, как правило, немного сложнее, так как требуется переписать файл пакета.
- В Windows записи в
.git\objects
папка, как правило, файлы только для чтения.File.Delete
не может удалить их в этом состоянии. Вам придется сначала сбросить атрибут "только для чтения", вызвавFile.SetAttributes(path, FileAttributes.Normal);
, например. - Несмотря на то, что вы можете определить, какие коммиты были переписаны, вы можете определить, какие из них висят / недоступны.
Tree
с иBlob
Это может превратиться в довольно сложную задачу.
В соответствии с предложениями, приведенными выше, приведен предварительный (все еще тестируемый) код C#, который я придумал, который усекает основную ветку в конкретном SHA, создавая новый начальный коммит. Это также удаляет все свисающие ссылки и BLOB-объекты
public class RepositoryUtility
{
public RepositoryUtility()
{
}
public String[] GetPaths(Commit commit)
{
List<String> paths = new List<string>();
RecursivelyGetPaths(paths, commit.Tree);
return paths.ToArray();
}
private void RecursivelyGetPaths(List<String> paths, Tree tree)
{
foreach (TreeEntry te in tree)
{
paths.Add(te.Path);
if (te.TargetType == TreeEntryTargetType.Tree)
{
RecursivelyGetPaths(paths, te.Target as Tree);
}
}
}
public void TruncateCommits(String repositoryPath, Int32 maximumCommitCount)
{
IRepository repository = new Repository(repositoryPath);
Int32 count = 0;
string newInitialCommitSHA = null;
foreach (Commit masterCommit in repository.Head.Commits)
{
count++;
if (count == maximumCommitCount)
{
newInitialCommitSHA = masterCommit.Sha;
}
}
//there must be parent commits to the commit we want to set as the new initial commit
if (count > maximumCommitCount)
{
TruncateCommits(repository, repositoryPath, newInitialCommitSHA);
}
}
private void RecursivelyCheckTreeItems(Tree tree,Dictionary<String, TreeEntry> treeItems, Dictionary<String, GitObject> gitObjectDeleteList)
{
foreach (TreeEntry treeEntry in tree)
{
//if the blob does not exist in a commit before the truncation commit then add it to the deletion list
if (!treeItems.ContainsKey(treeEntry.Target.Sha))
{
if (!gitObjectDeleteList.ContainsKey(treeEntry.Target.Sha))
{
gitObjectDeleteList.Add(treeEntry.Target.Sha, treeEntry.Target);
}
}
if (treeEntry.TargetType == TreeEntryTargetType.Tree)
{
RecursivelyCheckTreeItems(treeEntry.Target as Tree, treeItems, gitObjectDeleteList);
}
}
}
private void RecursivelyAddTreeItems(Dictionary<String, TreeEntry> treeItems, Tree tree)
{
foreach (TreeEntry treeEntry in tree)
{
//check for existance because if a file is renamed it can exist under a tree multiple times with the same SHA
if (!treeItems.ContainsKey(treeEntry.Target.Sha))
{
treeItems.Add(treeEntry.Target.Sha, treeEntry);
}
if (treeEntry.TargetType == TreeEntryTargetType.Tree)
{
RecursivelyAddTreeItems(treeItems, treeEntry.Target as Tree);
}
}
}
private void TruncateCommits(IRepository repository, String repositoryPath, string newInitialCommitSHA)
{
//get a repository object
Dictionary<String, TreeEntry> treeItems = new Dictionary<string, TreeEntry>();
Commit selectedCommit = null;
Dictionary<String, GitObject> gitObjectDeleteList = new Dictionary<String, GitObject>();
//loop thru the commits starting at the head moving towards the initial commit
foreach (Commit masterCommit in repository.Head.Commits)
{
//if non null then we have already found the commit where we want the truncation to occur
if (selectedCommit != null)
{
//since this is a commit after the truncation point add it to our deletion list
gitObjectDeleteList.Add(masterCommit.Sha, masterCommit);
//check the blobs of this commit to see if they should be deleted
RecursivelyCheckTreeItems(masterCommit.Tree, treeItems, gitObjectDeleteList);
}
else
{
//have we found the commit that we want to be the initial commit
if (String.Equals(masterCommit.Sha, newInitialCommitSHA, StringComparison.CurrentCultureIgnoreCase))
{
selectedCommit = masterCommit;
}
//this commit is before the new initial commit so record the tree entries that need to be kept.
RecursivelyAddTreeItems(treeItems, masterCommit.Tree);
}
}
//this function simply clears out the parents of the new initial commit
Func<Commit, IEnumerable<Commit>> rewriter = (c) => { return new Commit[0]; };
//perform the rewrite
repository.Refs.RewriteHistory(new RewriteHistoryOptions() { CommitParentsRewriter = rewriter }, selectedCommit);
//clean up references now in origional and remove the commits that they point to
foreach (var reference in repository.Refs.FromGlob("refs/original/*"))
{
repository.Refs.Remove(reference);
//skip branch reference on file deletion
if (reference.CanonicalName.IndexOf("master", 0, StringComparison.CurrentCultureIgnoreCase) == -1)
{
//delete the Blob from the file system
DeleteGitBlob(repositoryPath, reference.TargetIdentifier);
}
}
//now remove any tags that reference commits that are going to be deleted in the next step
foreach (var reference in repository.Refs.FromGlob("refs/tags/*"))
{
if (gitObjectDeleteList.ContainsKey(reference.TargetIdentifier))
{
repository.Refs.Remove(reference);
}
}
//remove the commits from the GIT ObectDatabase
foreach (KeyValuePair<String, GitObject> kvp in gitObjectDeleteList)
{
//delete the Blob from the file system
DeleteGitBlob(repositoryPath, kvp.Value.Sha);
}
}
private void DeleteGitBlob(String repositoryPath, String blobSHA)
{
String shaDirName = System.IO.Path.Combine(System.IO.Path.Combine(repositoryPath, ".git\\objects"), blobSHA.Substring(0, 2));
String shaFileName = System.IO.Path.Combine(shaDirName, blobSHA.Substring(2));
//if the directory exists
if (System.IO.Directory.Exists(shaDirName))
{
//get the files in the directory
String[] directoryFiles = System.IO.Directory.GetFiles(shaDirName);
foreach (String directoryFile in directoryFiles)
{
//if we found the file to delete
if (String.Equals(shaFileName, directoryFile, StringComparison.CurrentCultureIgnoreCase))
{
//if readonly set the file to RW
FileInfo fi = new FileInfo(shaFileName);
if (fi.IsReadOnly)
{
fi.IsReadOnly = false;
}
//delete the file
File.Delete(shaFileName);
//eliminate the directory if only one file existed
if (directoryFiles.Length == 1)
{
System.IO.Directory.Delete(shaDirName);
}
}
}
}
}
}
Спасибо за всю твою помощь. Это искренне ценится. Обратите внимание, что я отредактировал этот код из оригинала, потому что он не учитывал каталоги.