GV

GeezerVison

The World Wide Web, restored to 800 pixels of glory.

Home|About|Gaming cabinet|Archives|How it works|Starter sites

On this site

  • Sign in
  • ActionsCI/CD and automation
  • CodespacesCloud development environments
  • All Features
  • Documentation
  • Training
  • Certifications
  • Build with Copilot
  • Customize Copilot
  • Integrations
  • Trending repos
  • GitHub Sponsors
  • Developer articles
  • Enterprises

This view

  • Original page
  • New address

GeezerVison is showing github.com — GitPython `repo.index.commit()` spawns persistent git.exe instance, holds handles to repo · Issue #553 · gitpython-developers/GitPython

GitPython repo.index.commit() spawns persistent git.exe instance, holds handles to repo #553

New issue
Copy link
New issue
Copy link
Closed
Closed
GitPython repo.index.commit() spawns persistent git.exe instance, holds handles to repo#553
Copy link
Labels
acknowledgedtag.Windowstag.leaks

Description

@ghost
ghost
opened on Dec 2, 2016

I am trying to use GitPython for some repo manipulation, but ran into issues with my app, with handles open where i wouldn't expect.

Bug-jarring the issue, it seems that calling repo.index.commit() results in a several (4, consistently) git.exe processes being spawned, each holding a handle to the repo's root directory newRepo. When the test below goes to delete this TempDir, the processes are still up, causing a failure on context-manager __exit__(). My app occasionally needs to do a similar cleanup, so hits the same issue.

On the one hand, it looks like there is no contect-manager capable repo-wrapper, meaning it makes sense if some resources is open, it will not typically be cleaned/GC'd before the tempdir __exit__(). On the other hand - if Repo is going to behave like that, it really should not persist resources that have such side-effects.

Here is a working unittest:

import unittest
import git
import tempfile
import os.path

class Test(unittest.TestCase):

    def testCreateRepo(self):
        with tempfile.TemporaryDirectory(prefix=(__loader__.name) + "_") as mydir:

            # MAKE NEW REPO 
            repo = git.Repo.init(path=os.path.join(mydir, "newRepo"), mkdir=True)
            self.assertTrue(os.path.isdir(os.path.join(repo.working_dir, ".git")), "Failed to make new repo?")
            
            # MAKE FILE, COMMIT REPO
            testFileName = "testFile.txt"
            open(os.path.join(repo.working_dir, testFileName) , "w").close()
            repo.index.add([testFileName])
            self.assertTrue(repo.is_dirty())
            
            #### 
            # COMMENTING THIS OUT --> TEST PASSES
            repo.index.commit("added initial test file") 
            self.assertFalse(repo.is_dirty())
            #### 
            
            # adding this does not affect the handle
            git.cmd.Git.clear_cache()
            
            
            print("done") # exception thrown right after this, on __exit__
            # i can also os.walk(topdown=False) and delete all files and dirs (including .git/)
            # it is just the  newRepo/  folder itself who's handle is held open
            
if __name__ == '__main__':
    unittest.main()

PermissionError: [WinError 32] The process cannot access the file because it is being used by another process: 'C:\Users\%USER%\AppData\Local\Temp\EXAMPLE_gitpython_v3kbrly_\newRepo'

digging a little deeper, it seems that gitPython spawns multiple instances of git.exe processes, and each of them holds a handle to the root folder of the repo newRepo.

  • set a breakpoint immediately before the error, use sysinternals/handle to see open handles to newRepo ... git.exe (4 separate PID's of git.exe to be precise)
  • using sysinternals/procexp i can see that that they are all spawned from the python instance
    -- I'm typically running this from PyDev, but i verified the issue reproduces under vanila command line invocation of python.exe as well
  • the exception indicates it's a handle to newRepo being held. Adding a little extra code to the above I think that is the only handle held. I am able to successfully os.remove/os.rmdir() every dir and file, including all of .git/; and i finally manually recreate the issue seen on exit() in my example when i os.rmdir(newRepo)

stepping through, it's the call to repo.index.commit() that actually leads to the the git.exe(s) being spawned.

Activity

  1. ankostis
    added
    tag.Windows
    tag.leaks
    on Dec 2, 2016
  2. ankostis commented on Dec 2, 2016

    @ankostis
    ankostis
    on Dec 2, 2016
    Contributor
    More actions

    That's a quagmire of bugs!

    1. Each git-repo instance indeed holds persistent git commands, that are supposed to be cleared by repo.clear_cache(). Otherwise, on Windows only, these commands prevent the repo-dir from being deleted.
      So you have to invoke repo.clear_cache().
      But that alone won't work unless you have garbage-collect first!

    2. Additionally, the temp-file still may not be deleted because it contains read-only files (the blobs in .git/objects), so you need extra code for that. This code exists in git.util.rmtree() but currently git.util modules is being masked by git.index.util module, due to a bug, so you cannot use it - you have to copy it :-(

    3. Finally, your code also had an error, you have to invoke clear_cache() on the repo instance, not directly on Git class.

    So now the code becomes:

    import unittest
    import gc
    import git
    import tempfile
    import os.path
    import shutil
    import stat
    
    
    def rmtree(path):
        """Remove the given recursively.
    
        :note: we use shutil rmtree but adjust its behaviour to see whether files that
            couldn't be deleted are read-only. Windows will not remove them in that case"""
    
        def onerror(func, path, exc_info):
            # Is the error an access error ?
            os.chmod(path, stat.S_IWUSR)
            print('dfsffdss')
            try:
                func(path)  # Will scream if still not possible to delete.
            except Exception as ex:
                raise
    
        return shutil.rmtree(path, False, onerror)
    
    
    class Test(unittest.TestCase):
    
        def testCreateRepo(self):
            with tempfile.TemporaryDirectory(prefix=(__loader__.name) + "_") as mydir:
    
                # MAKE NEW REPO
                repo = git.Repo.init(path=os.path.join(mydir, "newRepo"), mkdir=True)
                try:
                    self.assertTrue(os.path.isdir(os.path.join(repo.working_dir, ".git")), "Failed to make new repo?")
    
                    # MAKE FILE, COMMIT REPO
                    testFileName = "testFile.txt"
                    open(os.path.join(repo.working_dir, testFileName) , "w").close()
                    repo.index.add([testFileName])
                    self.assertTrue(repo.is_dirty())
    
                    repo.index.commit("added initial test file")
                    self.assertFalse(repo.is_dirty())
    
                    print("done")
    
                finally:
                    gc.collect()
                    repo.git.clear_cache()
                    rmtree(repo.git_dir)
    
    if __name__ == '__main__':
        unittest.main()
    

    Please report if everything has been solved.

    Reacted by Pooja Bhat and allinone-1
  3. ankostis commented on Dec 2, 2016

    @ankostis
    ankostis
    on Dec 2, 2016
    Contributor
    More actions

    We would have fixed it yesterday, if we could :-)

    Just search for tag.leaks to get an idea of the invested effort to reach to the point where git-python runs decently in Windows on PY34+.

    The 1st and easiest fix is to retrofit git.Repo as a context-manager, so instead of try..finally you would use it as with Repo() as repo: ....

  4. ankostis
    added 2 commits that reference this issue on Dec 8, 2016

    fix(leaks): repo context-man to cleanup global mman on repo-delete

    f1a82e4

    feat(util): export rmtree() to facilitate del of tmp-repos on Windows

    66306f1
  5. ankostis commented on Dec 8, 2016

    @ankostis
    ankostis
    on Dec 8, 2016
    Contributor
    More actions

    Assuming #555 gets merged in the next release, in your case the code would become simply like that:

    class Test(unittest.TestCase):
    
        def testCreateRepo(self):
            with tempfile.TemporaryDirectory(prefix=(__loader__.name) + "_") as mydir:
                with git.Repo.init(path=os.path.join(mydir, "newRepo"), mkdir=True) as repo:
                    self.assertTrue(os.path.isdir(os.path.join(repo.working_dir, ".git")), "Failed to make new repo?")
    
                    # MAKE FILE, COMMIT REPO
                    testFileName = "testFile.txt"
                    open(os.path.join(repo.working_dir, testFileName) , "w").close()
                    repo.index.add([testFileName])
                    self.assertTrue(repo.is_dirty())
    
                    repo.index.commit("added initial test file")
                    self.assertFalse(repo.is_dirty())
    
                    print("done")
  6. Byron
    added
    acknowledged
    on Dec 8, 2016
  7. Byron commented on Dec 8, 2016

    @Byron
    Byron
    on Dec 8, 2016
    Member
    More actions

    @mboard182 It looks like the context-manager addition is in-flight and might make it in the release planned for today.

  8. emonty
    added a commit that references this issue on Mar 8, 2017

    Perform the first gc.collect before clearing cache

    0533dd6
  9. jeblair
    added a commit that references this issue on Oct 9, 2017

    Only gc.collect() under windows

    This issue will close once commit 454feda is merged into the 'master' branch.
    454feda
  10. Byron
    closed this as completedin 454fedaon Nov 2, 2017
  11. riley-martine
    added a commit that references this issue on Dec 7, 2023

    Only gc.collect() under windows

    This issue will close once commit a98c653 is merged into the 'master' branch.
    a98c653
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    acknowledgedtag.Windowstag.leaks

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions

      You are visitor 01402503 — GeezerVison does not store the pages you look up.
      Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required.