Использование LibGit2Sharp для усечения истории коммитов GIT

Я планирую использовать LibGit2/LibGit2Sharp и, следовательно, GIT неортодоксальным образом, и я прошу всех, кто знаком с API, подтвердить, что то, что я предлагаю, теоретически будет работать. :)

Сценарий

В репозитории будет существовать только основная ветвь. Большое количество каталогов, содержащих большие двоичные и недвоичные файлы, будет отслеживаться и фиксироваться. Большинство бинарных файлов будут изменяться между фиксациями. Репозиторий должен содержать не более 10 коммитов из-за нехватки места на диске (диск сейчас довольно часто заполняется).

Чего API не предоставляет, так это функции, которая обрезает историю коммитов, начиная с указанного CommitId, до исходного коммита основной ветки и удаляет все объекты GIT, которые в результате будут зависать.

Я протестировал метод ReferenceCollection.RewiteHistory и могу использовать его для удаления родителей из фиксации. Это создает новую историю коммитов, начиная с CommitId и возвращаясь к HEAD. Но это по-прежнему оставляет все старые коммиты и любые ссылки или BLOB-объекты, которые уникальны для этих коммитов. Мой план прямо сейчас состоит в том, чтобы просто очистить эти оборванные объекты GIT самостоятельно. Кто-нибудь видит какие-либо проблемы с этим подходом или имеет лучший?


person user3092651    schedule 11.12.2013    source источник


Ответы (2)


Но это по-прежнему оставляет все старые коммиты и любые ссылки или BLOB-объекты, которые уникальны для этих коммитов. Мой план прямо сейчас состоит в том, чтобы просто очистить эти оборванные объекты GIT самостоятельно.

Переписывая историю репозитория, LibGit2Sharp заботится о том, чтобы не сбросить переписанную ссылку. Пространство имен, в котором они хранятся, по умолчанию refs/original. Это можно изменить с помощью параметра RewriteHistoryOptions.

Чтобы удалить старые коммиты, деревья и блобы, нужно сначала удалить эти ссылки. Этого можно добиться с помощью следующего кода:

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 может превратиться в довольно сложную задачу.
person nulltoken    schedule 11.12.2013
comment
Хранит ли Lib2GitSharp файлы в упакованном формате? Я видел, где он может принимать упакованные файлы в репозиторий, но я не видел какого-либо конкретного API, который мог бы вызвать запаковку. Если мне приходится работать с упакованными файлами, моя работа значительно усложняется. - person user3092651; 13.12.2013
comment
Кроме того, если файлы упаковываются, могу ли я предотвратить такое поведение? - person user3092651; 13.12.2013

В соответствии с приведенными выше предложениями здесь приведен предварительный (все еще тестируемый) код 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);
                    }
                }
            }
        }
    }
}

Спасибо за всю твою помощь. Это искренне ценится. Обратите внимание, что я отредактировал этот код из оригинала, потому что он не учитывал каталоги.

person user3092651    schedule 12.12.2013