【C#】リフレクションを使用して特定インスタンスが指定型のフィールドを持っているか再帰的に調べる

特定インスタンスのフィールドに指定型が含まれているか調べるコードです。
フィールドの中のフィールドも調べます。

試した感じですが, フィールドを省略してプロパティを定義していても取得できるっぽかったです。
↓みたいなやつ

public Hoge Hoge { get; set; }

ソースコード

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;

public static class ReflectionHelper
{
    private static Dictionary<Type, CacheData> Cache = new();

    private static CacheData SafeGetCache<T>()
    {
        if (!Cache.TryGetValue(typeof(T), out var cacheData))
        {
            cacheData = new CacheData();
            Cache.Add(typeof(T), cacheData);
        }

        return cacheData;
    }

    /// <summary>
    /// instance の持つ T 型の インスタンス を全て(T, List<T>, T[] が対象)取得する.
    /// </summary>
    public static List<T> GetAllTInstancies<T>(object instance) where T : class
    {
        var result = new List<T>();

        foreach (var kv in GetInstanceToFieldInfo<T>(instance))
        {
            foreach (var fieldInfo in kv.Value)
            {
                var i = fieldInfo.GetValue(kv.Key);
                if (fieldInfo.FieldType.IsGenericType)
                {
                    if (fieldInfo.FieldType.GetGenericTypeDefinition() == typeof(List<>))
                    {
                        result.AddRange((IEnumerable<T>)i);
                    }
                }
                else if (fieldInfo.FieldType.IsArray)
                {
                    result.AddRange((T[])i);
                }
                else
                {
                    result.Add((T)i);
                }
            }
        }

        return result.Distinct().ToList();
    }

    /// <summary>
    /// instance の T 型の FieldInfo を全て取得する.
    /// instance のベースクラスの FieldInfo も含みます.
    /// フィールドのフィールドもチェックします.
    /// フィールドのベースクラスのフィールドもチェックします.
    /// </summary>
    public static Dictionary<object, HashSet<FieldInfo>> GetInstanceToFieldInfo<T>(object instance)
    {
        var result = new Dictionary<object, HashSet<FieldInfo>>();
        GetInstanceToFieldInfo<T>(instance, instance.GetType(), result);
        return result;
    }

    private static void GetInstanceToFieldInfo<T>(object instance, Type type, Dictionary<object, HashSet<FieldInfo>> result)
    {
        if (instance == null)
        {
            return;
        }

        if (type == null)
        {
            return;
        }

        if (!HasField<T>(type))
        {
            return;
        }

        foreach (var field in type.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance))
        {
            var fieldType = field.FieldType;
            if (DependenceToT<T>(fieldType, SafeGetCache<T>().SkipTypeCheckList))
            {
                SafeAdd(instance, field);
            }

            // フィールドのベースクラスのフィールドをチェック.
            GetInstanceToFieldInfo<T>(field.GetValue(instance), fieldType.BaseType, result);

            // フィールドのフィールドをチェック.
            GetInstanceToFieldInfo<T>(field.GetValue(instance), fieldType, result);
        }

        return;

        void SafeAdd(object instance, FieldInfo fieldInfo)
        {
            if (!result.TryGetValue(instance, out var hashSet))
            {
                hashSet = new HashSet<FieldInfo>();
                result.Add(instance, hashSet);
            }

            hashSet.Add(fieldInfo);
        }
    }

    /// <summary>
    /// type の Field に T があるか.
    /// Field の Field を再帰的にチェックします.
    /// Field の ベースクラスの Field もチェックします.
    /// </summary>
    public static bool HasField<T>(Type type)
    {
        return HasField<T>(type, null);
    }

    private static bool HasField<T>(Type type, HashSet<Type> alreadyChecked)
    {
        if (DependenceToT<T>(type, SafeGetCache<T>().SkipTypeCheckList))
        {
            return true;
        }

        alreadyChecked ??= new HashSet<Type>();
        if (alreadyChecked.Contains(type))
        {
            return false;
        }

        alreadyChecked.Add(type);
        foreach (var field in type.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance))
        {
            // フィールドのフィールドをチェック.
            if (HasField<T>(field.FieldType, alreadyChecked))
            {
                return true;
            }
        }

        return false;
    }

    /// <summary>
    /// type が T に依存しているか.
    /// type[], List<type>, List<type>[] の場合も true を返します.
    /// type のベースクラスが T の場合も true を返します.
    /// </summary>
    private static bool DependenceToT<T>(Type type, HashSet<Type> skipTypeList, HashSet<Type> alreadyChecked = null)
    {
        // 無限ループ対策. A : B<A> など.
        alreadyChecked ??= new HashSet<Type>();
        if (alreadyChecked.Contains(type))
        {
            return false;
        }

        alreadyChecked.Add(type);
        if (skipTypeList.Contains(type))
        {
            return false;
        }
        if (type == null)
        {
            return false;
        }
        if (type == typeof(object))
        {
            return false;
        }
        if (type == typeof(T))
        {
            return true;
        }
        if (type.IsGenericType)
        {
            foreach (var arg in type.GetGenericArguments())
            {
                if (arg == typeof(T))
                {
                    return true;
                }

                if (DependenceToT<T>(arg, skipTypeList, alreadyChecked))
                {
                    return true;
                }
                else
                {
                    skipTypeList.Add(arg);
                }
            }
        }
        if (type.IsArray)
        {
            var elementType = type.GetElementType();
            if (elementType == typeof(T))
            {
                return true;
            }

            type = elementType;
        }

        if (DependenceToT<T>(type.BaseType, skipTypeList, alreadyChecked))
        {
            return true;
        }

        skipTypeList.Add(type);
        return false;
    }

    private class CacheData
    {
        public HashSet<Type> SkipTypeCheckList = new HashSet<Type>();
    }
}

【Unity】AnimationClip のリネームサポートツール

Animator を使用している GameObject の子の命名を変えると,
AnimationClip で Missing が発生してしまい, 修正が手間なため, ツールを作成しました。

完成形

存在するパスが入力されると緑色のアイコンが表示されます。

ソースコード

using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
using System.Linq;

namespace Test_AnimationMissing
{
    public class AnimationFixer : EditorWindow
    {
        private Animator m_Animator;
        private List<AnimationClip> m_MissingClips = new();
        private List<string> m_MissingPathList = new();
        private List<string> m_ReplaceList = new();
        private List<string> m_ExistsPathList = new();
        private Vector2 m_ScrollPosition;
        private Texture2D m_SuccessIcon;
        private Texture2D m_FailedIcon;

        [MenuItem("EditorWindow/AnimationFixer")]
        private static void Init()
        {
            var window = GetWindow(typeof(AnimationFixer));
            window.titleContent = new GUIContent("AnimationFixer");
            window.Show();
            window.minSize = new Vector2(330, 400);
        }

        private void OnEnable()
        {
            m_SuccessIcon = (Texture2D)EditorGUIUtility.Load("d_greenLight");
            m_FailedIcon = (Texture2D)EditorGUIUtility.Load("d_redLight");
        }

        private void OnGUI()
        {
            using (var check = new EditorGUI.ChangeCheckScope())
            {
                m_Animator = EditorGUILayout.ObjectField("animator", m_Animator, typeof(Animator), true) as Animator;

                if (check.changed)
                {
                    Reload();
                }
            }

            if (m_Animator == null)
            {
                EditorGUILayout.HelpBox("animator を設定してください", MessageType.Info);
                return;
            }

            using (new EditorGUILayout.HorizontalScope())
            {
                GUILayout.FlexibleSpace();
                if (GUILayout.Button("リロード", GUILayout.Width(80)))
                {
                    Reload();
                }
            }

            using (var scrollview = new EditorGUILayout.ScrollViewScope(m_ScrollPosition))
            {
                m_ScrollPosition = scrollview.scrollPosition;
                if (m_MissingClips.Count == 0)
                {
                    EditorGUILayout.HelpBox("Missing がありません", MessageType.Info);
                    return;
                }

                using (new EditorGUILayout.HorizontalScope())
                {
                    GUILayout.Label("■ Missing パス");
                    GUILayout.Space(10);
                    GUILayout.Label("■ 置換後 パス");
                }
                using (new EditorGUILayout.VerticalScope("box"))
                {
                    for (int i = 0; i < m_MissingPathList.Count; i++)
                    {
                        using (new EditorGUILayout.HorizontalScope())
                        {
                            EditorGUILayout.SelectableLabel(m_MissingPathList[i], GUILayout.Height(15));
                            GUILayout.Label("->", GUILayout.Width(20));
                            m_ReplaceList[i] = EditorGUILayout.TextField(m_ReplaceList[i]);
                            var icon = m_ExistsPathList.Contains(m_ReplaceList[i]) ? m_SuccessIcon : m_FailedIcon;
                            if (icon != null)
                            {
                                GUILayout.Label(new GUIContent(icon), GUILayout.Height(20), GUILayout.Width(20));
                            }
                        }
                    }
                }

                GUILayout.Space(15);

                GUILayout.Label("■ 修正対象のAnimationClip");
                foreach (var clip in m_MissingClips)
                {
                    EditorGUILayout.ObjectField(clip, typeof(AnimationClip), false);
                }

                if (GUILayout.Button("Apply"))
                {
                    for (int i = 0; i < m_MissingPathList.Count; i++)
                    {
                        var missingPath = m_MissingPathList[i];
                        var replacePath = m_ReplaceList[i];
                        Undo.RecordObjects(m_MissingClips.ToArray(), "replace animationclip paths");
                        ReplacePath(m_MissingClips.ToArray(), missingPath, replacePath);
                    }

                    Reload();
                }
            }
        }


        private void Reload()
        {
            GUI.FocusControl(null);
            m_ReplaceList.Clear();
            m_MissingClips.Clear();
            m_MissingPathList.Clear();

            var result = GetMissingInfo(m_Animator);
            m_MissingClips.AddRange(result.Item2);
            m_MissingPathList.AddRange(result.Item1);
            m_ReplaceList.AddRange(m_MissingPathList);
            m_ExistsPathList = GetAllPaths(m_Animator.transform);
        }

        // List<missing path>, List<missing clip>
        private static (List<string>, List<AnimationClip>) GetMissingInfo(Animator m_Animator)
        {
            var result = (new List<string>(), new List<AnimationClip>());
            var controller = m_Animator.runtimeAnimatorController as UnityEditor.Animations.AnimatorController;
            var clips = controller.animationClips.Distinct().ToList();

            foreach (var clip in clips)
            {
                var hasMissing = false;
                var bindings = AnimationUtility.GetCurveBindings(clip);
                foreach (var binding in bindings)
                {
                    if (m_Animator.transform.Find(binding.path) == null)
                    {
                        result.Item1.Add(binding.path);
                        hasMissing = true;
                    }
                }

                if (hasMissing)
                {
                    result.Item2.Add(clip);
                }
            }

            result.Item1 = result.Item1.Distinct().ToList();
            result.Item2 = result.Item2.Distinct().ToList();

            return result;
        }

        private static void ReplacePath(AnimationClip[] clips, string oldPath, string newPath)
        {
            foreach (var clip in clips)
            {
                var bindings = AnimationUtility.GetCurveBindings(clip);
                var removeBindings = bindings.Where(c => c.path.Contains(oldPath));

                foreach (var binding in removeBindings)
                {
                    var curve = AnimationUtility.GetEditorCurve(clip, binding);
                    var newBinding = binding;
                    newBinding.path = newBinding.path.Replace(oldPath, newPath);
                    AnimationUtility.SetEditorCurve(clip, binding, null);
                    AnimationUtility.SetEditorCurve(clip, newBinding, curve);
                }
            }
        }

        private static List<string> GetAllPaths(Transform root)
        {
            List<string> paths = new List<string>();
            // Bindings の path に root は含まれない
            foreach (Transform child in root)
            {
                AddPathsRecursive(child, null, paths);
            }
            return paths;
        }

        private static void AddPathsRecursive(Transform current, string path, List<string> paths)
        {
            path = string.IsNullOrEmpty(path) ? current.name : path + "/" + current.name;
            paths.Add(path);
            foreach (Transform child in current)
            {
                AddPathsRecursive(child, path, paths);
            }
        }
    }
}

参考

以下リンク先をかなり参考にさせていただいていますm
tsubakit1.hateblo.jp

【Unity】特定の型のアセットの依存情報をJSONで保存する

特定の型の依存情報のみを高速で取得したい案件があったので,
JSONで依存情報を保存しておき, 必要な時にデシリアライズして使用します.

ソースコード

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using UnityEditor;
using UnityEngine;

namespace DependenceData
{
    public class DependenceData : AssetPostprocessor
    {
        private const string ROOT_DIR_NAME = "DependenceData";
        private static List<Type> ObserveType = new List<Type>() { typeof(MonoScript), typeof(GameObject), };

        [MenuItem("Func/DependenceData")]
        private static void Execute()
        {
            foreach (var type in ObserveType)
            {
                Initialize(type);
            }
        }

        public static void Initialize(Type type)
        {
            var guids = AssetDatabase.FindAssets($"t: {type.Name}").ToList();
            UpdateGuids(guids);
        }

        private static void AddGuid(string addedGuid)
        {
            // 依存先の被依存リストに追加.
            var guidList = GetDependenciesAsGuidByGuid(addedGuid);
            foreach (var guid in guidList)
            {
                TryGetSavedGuids(guid, out var savedGuids);
                savedGuids ??= new Guids(); // ファイルが無ければ作成.
                savedGuids.AddFromDependents(addedGuid);

                Save(guid, savedGuids);
            }

            // 依存先情報を保存.
            var addedGuids = new Guids();
            addedGuids.UpdateToDependencies(guidList);
            Save(addedGuid, addedGuids);
        }

        private static void RemoveGuids(List<string> removedGuidList)
        {
            if (removedGuidList == null)
            {
                return;
            }

            foreach (var removedGuid in removedGuidList)
            {
                // 依存先の被依存リストから削除.
                TryGetSavedGuids(removedGuid, out var guids);
                guids ??= new Guids();
                foreach (var guid in guids.m_ToDependencies)
                {
                    if (TryGetSavedGuids(guid, out var savedGuids))
                    {
                        savedGuids.RemoveFromDependents(removedGuid);
                        Save(guid, savedGuids);
                    }
                }

                DeleteSaveData(removedGuid);
            }
        }

        private static void UpdateGuids(List<string> guidList)
        {
            if (guidList == null)
            {
                return;
            }

            foreach (var guid in guidList)
            {
                var dependencies = GetDependenciesAsGuidByGuid(guid);
                // 既に管理対象の guid であれば, 関連ファイルを更新する.
                if (TryGetSavedGuids(guid, out var savedGuids))
                {
                    foreach (var added in dependencies)
                    {
                        // 依存先の被依存リストに書き込む.
                        TryGetSavedGuids(added, out var guids);
                        guids ??= new Guids();
                        guids.AddFromDependents(guid);
                        Save(added, guids);
                    }

                    foreach (var removed in savedGuids.GetExceptedToDependencies(dependencies))
                    {
                        // 依存先の被依存から消す.
                        if (TryGetSavedGuids(removed, out var guids))
                        {
                            guids.RemoveFromDependents(guid);
                            Save(removed, guids);
                        }
                    }

                    savedGuids.UpdateToDependencies(dependencies);
                    Save(guid, savedGuids);
                }
                else
                {
                    // 新規追加の guid.
                    AddGuid(guid);
                }
            }
        }

        private static IEnumerable<string> GetDependenciesAsGuidByGuid(string guid)
        {
            var path = AssetDatabase.GUIDToAssetPath(guid);
            return AssetDatabase.GetDependencies(path, false)
                .Select(path => AssetDatabase.AssetPathToGUID(path));
        }

        public static List<string> GetFromDependentsByPath(string path)
        {
            return GetFromDepenets(AssetDatabase.AssetPathToGUID(path));
        }

        public static List<string> GetFromDepenets(string guid)
        {
            TryGetSavedGuids(guid, out var guids);
            return guids.m_FromDependents ?? new List<string>();
        }

        private static bool TryGetSavedGuids(string guid, out Guids guids)
        {
            guids = null;
            var filePath = GetGuidPath(guid);
            if (File.Exists(filePath))
            {
                var json = File.ReadAllText(filePath);
                guids = JsonUtility.FromJson<Guids>(json);
                return true;
            }

            return false;
        }

        private static void Save(string fileGuid, Guids guids)
        {
            var filePath = GetGuidPath(fileGuid);
            CreateDirectory(filePath);
            var json = JsonUtility.ToJson(guids, true);
            File.WriteAllText(filePath, json);
        }

        private static void DeleteSaveData(string guid)
        {
            var path = GetGuidPath(guid);
            if (!File.Exists(path))
            {
                return;
            }

            File.Delete(path);
        }

        private static string GetRootPath()
        {
            var projectPath = Directory.GetParent(Application.dataPath).FullName.Replace("\\", "/");
            return $"{projectPath}/{ROOT_DIR_NAME}";
        }

        private static string GetGuidPath(string guid)
        {
            return $"{GetRootPath()}/{guid.Substring(0, 2)}/{guid}.json";
        }

        private static void CreateDirectory(string filePath)
        {
            var dir = Directory.GetParent(filePath).FullName.Replace("\\", "/");
            if (Directory.Exists(dir))
            {
                return;
            }

            Directory.CreateDirectory(dir);
        }

        private static void OnPostprocessAllAssets(string[] importedAssets, string[] deletedAssets, string[] movedAssets, string[] movedFromAssetPaths)
        {
            var importedAssetsGuids = importedAssets
                .Where(path => ObserveType.Contains(AssetDatabase.GetMainAssetTypeAtPath(path)))
                .Select(path => AssetDatabase.AssetPathToGUID(path))
                .ToList();
            UpdateGuids(importedAssetsGuids);

            var deletedAssetsGuids = deletedAssets
                .Where(path => ObserveType.Contains(AssetDatabase.GetMainAssetTypeAtPath(path)))
                .Select(path => AssetDatabase.AssetPathToGUID(path))
                .ToList();
            RemoveGuids(deletedAssetsGuids);
        }

        [Serializable]
        private class Guids
        {
            // 被依存
            public List<string> m_FromDependents = new();
            // 依存先
            public List<string> m_ToDependencies = new();

            public Guids() { }

            // 非依存を追加
            public void AddFromDependents(string guid)
            {
                if (m_FromDependents.Contains(guid))
                {
                    return;
                }

                m_FromDependents.Add(guid);
            }

            // 被依存を削除
            public void RemoveFromDependents(string guid)
            {
                m_FromDependents.Remove(guid);
            }

            // 被依存を削除
            public void RemoveFromDependents(Guids guids)
            {
                if (guids == null)
                {
                    return;
                }

                foreach (var guid in guids.m_FromDependents)
                {
                    m_FromDependents.Remove(guid);
                }
            }

            // 依存先を更新.
            public void UpdateToDependencies(IEnumerable<string> guidList)
            {
                if (guidList == null)
                {
                    return;
                }

                m_ToDependencies.Clear();
                m_ToDependencies.AddRange(guidList);
            }

            // 非依存・依存先を追加
            public void Add(Guids guids)
            {
                if (guids == null)
                {
                    return;
                }

                foreach (var guid in guids.m_FromDependents)
                {
                    AddFromDependents(guid);
                }

                UpdateToDependencies(guids.m_ToDependencies);
            }

            // guidList にのみ存在する guid リストを返す
            public IEnumerable<string> GetExceptedToDependencies(IEnumerable<string> guidList)
            {
                if (guidList == null)
                {
                    return new List<string>();
                }

                return m_ToDependencies.Except(guidList);
            }
        }
    }
}

【Unity】AnimationClip を path で検索できるツール

完成イメージ

アニメーションクリップをパスで検索したいケースがあったので作成しました。
要素を ドラッグ&ドロップして Inspector などの ObjectField にアタッチすることも可能です。

ソースコード

using System.Collections.Generic;
using System.Linq;
using UnityEditor;
using UnityEditor.IMGUI.Controls;
using UnityEngine;

public class AnimationClipSearch : EditorWindow
{
    [System.Serializable]
    public class AssetData<T> where T : Object
    {
        [SerializeField]
        private int m_Id;

        [SerializeField]
        private string m_Path;

        [SerializeField]
        private T m_Asset;

        [SerializeField]
        private Texture2D m_Icon;

        public int Id => m_Id;
        public string Path => m_Path;
        public T Asset => m_Asset;
        public Texture2D Icon => m_Icon;

        public AssetData(int id, string path, T asset)
        {
            m_Id = id;
            m_Path = path;
            m_Asset = asset;
            m_Icon = (Texture2D)EditorGUIUtility.ObjectContent(m_Asset, m_Asset.GetType()).image;
        }
    }

    [MenuItem("EditorWindow/AnimationClipSearch")]
    private static void Init()
    {
        var window = GetWindow(typeof(AnimationClipSearch));
        window.titleContent = new GUIContent("AnimationClipSearch");
        window.Show();
        window.minSize = new Vector2(330, 400);
    }

    [SerializeField]
    private List<AssetData<AnimationClip>> m_AssetDataList;
    [SerializeField]
    private TreeViewState m_TreeViewState;

    private AssetPathListView<AnimationClip> m_AssetPathListView;
    private SearchField m_SearchField;

    private void OnGUI()
    {
        using (new EditorGUILayout.HorizontalScope(EditorStyles.toolbar))
        {
            if (m_AssetDataList == null || GUILayout.Button("Reload", EditorStyles.toolbarButton, GUILayout.Width(80)))
            {
                m_AssetDataList = AssetDatabase.FindAssets("t:AnimationClip", new[] { "Assets/" })
                    .Select(guid => AssetDatabase.GUIDToAssetPath(guid))
                    .Select((path, index) => new AssetData<AnimationClip>(index, path, AssetDatabase.LoadAssetAtPath<AnimationClip>(path)))
                    .ToList();

                m_TreeViewState = null;
                m_AssetPathListView = null;
            }

            if (m_TreeViewState == null)
            {
                m_TreeViewState = new TreeViewState();
            }

            if (m_AssetPathListView == null)
            {
                m_AssetPathListView = new AssetPathListView<AnimationClip>(m_TreeViewState);
                m_AssetPathListView.Setup(m_AssetDataList);
                m_SearchField = new SearchField();
                m_SearchField.downOrUpArrowKeyPressed += m_AssetPathListView.SetFocusAndEnsureSelectedItem;
            }

            GUILayout.Space(100);
            GUILayout.FlexibleSpace();

            m_AssetPathListView.searchString = m_SearchField.OnToolbarGUI(m_AssetPathListView.searchString);
        }

        var rect = EditorGUILayout.GetControlRect(false, GUILayout.ExpandHeight(true));
        m_AssetPathListView.OnGUI(rect);
    }

    public class AssetPathListView<T> : TreeView where T : Object
    {
        private List<AssetData<T>> m_AssetDataList;

        public AssetPathListView(TreeViewState treeViewState) : base(treeViewState)
        {
            showAlternatingRowBackgrounds = true;
        }

        public void Setup(List<AssetData<T>> baseElements)
        {
            m_AssetDataList = baseElements;
            Reload();
        }

        protected override TreeViewItem BuildRoot()
        {
            var root = new TreeViewItem { id = -1, depth = -1, displayName = "Root" };

            foreach (var assetData in m_AssetDataList)
            {
                var item = new TreeViewItem { id = assetData.Id, displayName = assetData.Path, icon = assetData.Icon };
                root.AddChild(item);
            }

            SetupDepthsFromParentsAndChildren(root);
            return root;
        }

        protected override bool CanStartDrag(CanStartDragArgs args)
        {
            return true;
        }

        protected override void SetupDragAndDrop(SetupDragAndDropArgs args)
        {
            DragAndDrop.PrepareStartDrag();
            var selectedItems = args.draggedItemIDs;
            var objects = m_AssetDataList
                .Where(assetData => selectedItems.Contains(assetData.Id))
                .Select(assetData => assetData.Asset)
                .ToArray();

            DragAndDrop.objectReferences = objects;
            DragAndDrop.StartDrag("AnimationClipSearch Drag");
        }
    }
}

Git LFS 調査用メモ

Git LFS で何か踏んだ時に調べる際に, 参照する用のメモ.

環境変数

GIT_CURL_VERBOSE=1

内部で使用している, curl ライブラリが生成するメッセージを出力する.

GIT_TRANSFER_TRACE=1

issue とかでこれを設定しろって言っているのを見かける.

GIT_TRACE=1

どの特定のカテゴリにも当てはまらない, 一般的なトレースを制御する.
エイリアスの展開や, 他のサブプログラムへの処理の引き渡しなどが含まれる.

git lfs logs

ログファイルが一覧表示される github.com

参考

git-scm.com