11# coding=utf-8
22from __future__ import annotations
3+ import os
34
4- from git import Repo , exc , Actor
5+ from git import Repo , exc , Actor , Remote , Submodule
56
67from inginious .common .filesystems .local import LocalFSProvider
78
@@ -21,34 +22,65 @@ def from_subfolder(self, subfolder) -> GitFSProvider:
2122 self ._checkpath (subfolder )
2223 return GitFSProvider (self .prefix + subfolder )
2324
25+ def _is_prefix_allowed (self , prefix : str ) -> bool :
26+ # See https://github.com/gitpython-developers/GitPython/issues/832
27+ banned = ['$' , '%' ]
28+ allowed = True
29+ [allowed := allowed and c not in prefix for c in banned ]
30+ return allowed
31+
2432 def __init__ (self , prefix : str ):
2533 super ().__init__ (prefix )
26- # TODO: filter directories beginning with '$'.
2734 try :
28- self .repo = Repo (prefix )
35+ self .repo = Repo (prefix ) if self . _is_prefix_allowed ( prefix ) else None
2936 except exc .NoSuchPathError :
3037 self .repo = None
3138 except exc .InvalidGitRepositoryError :
3239 # tasks/ directory won't be initialized as a git repository.
3340 self .repo = None
3441
3542 def ensure_exists (self ) -> None :
36- self .repo = Repo .init (self .prefix , b = "main" )
37- with self .repo .config_writer () as cfg :
38- # TODO: Set user SSH key ?
39- pass
43+ if self ._is_prefix_allowed (self .prefix ):
44+ self .repo = Repo .init (self .prefix , b = "main" )
45+ if len ([remote for remote in self .repo .remotes if remote .name == 'origin' ]) == 0 :
46+ Remote .add (self .repo , 'origin' , f'ingitolite:{ self .prefix } ' )
47+
48+ def _should_stage (self , filepath : str ) -> bool :
49+ # We should stage if the filepath is tracked but has a diff, or if the
50+ # filepath is untracked.
51+ return False if self .repo is None else len (self .repo .index .diff (None , paths = [filepath ])) == 1 or filepath in self .repo .untracked_files
52+
53+ def _is_task (self , filepath : str ) -> bool :
54+ descriptors = [f'task.{ ext } ' for ext in ['yaml' , 'json' ]]
55+ is_task = False
56+ if os .path .isdir (filepath ):
57+ [is_task := is_task or descr in os .listdir (filepath ) for descr in descriptors ]
58+ return is_task
59+
60+ def _is_submodule (self , filepath : str ) -> bool :
61+ return len ([sm for sm in self .repo .submodules if sm .name == filepath ]) == 1
62+
63+ def _try_stage (self , filepath : str , should_stage : bool ) -> None :
64+ if should_stage :
65+ if self ._is_task (filepath ) and not self ._is_submodule (filepath ):
66+ # Add newly created tasks as course submodule.
67+ Submodule .add (self .repo , name = filepath , path = filepath )
68+ else :
69+ self .repo .git .add ([filepath ])
70+
71+ def try_stage (self , filepath : str ) -> None :
72+ self ._try_stage (filepath , self ._should_stage (filepath ))
4073
41- def _try_commit (self , filepath : str , msg : str = None , user : tuple [str , str ]= None ):
74+ def try_commit (self , filepath : str , msg : str = None , user : tuple [str , str ]= None ) -> None :
4275 # Should we stage the descriptor?
43- to_stage = len ( self .repo . index . diff ( None , paths = [ filepath ])) == 1 or filepath in self . repo . untracked_files
76+ should_stage = self ._should_stage ( filepath )
4477 # Is the descriptor already staged? We should have a valid HEAD to test
4578 # that case.
46- # FIXME: This may commit other staged files. Is that an issue?
47- descr_changed = to_stage or (self .repo .head .is_valid () and len (self .repo .index .diff ("HEAD" , paths = [filepath ])) == 1 )
79+ # This may commit other staged files, e.g., when adding a newly created
80+ # task.
81+ descr_changed = should_stage or (self .repo .head .is_valid () and len (self .repo .index .diff ("HEAD" , paths = [filepath ])) == 1 )
4882 if descr_changed :
49- if to_stage :
50- self .repo .index .add ([filepath ])
51- self .repo .index .write ()
83+ self ._try_stage (filepath , should_stage )
5284 if user is not None :
5385 actor = Actor (name = user [0 ], email = user [1 ])
5486 # This function is only called for writing course / task
@@ -65,20 +97,21 @@ def put(self, filepath: str, content, msg: str=None, user: tuple[str, str]=None)
6597 # This function is called each time the button 'save changes' is pressed
6698 # on the web GUI, even if the content is unchanged...
6799 # We check that content has indeed changed before trying to commit.
68- self ._try_commit (filepath , msg , user )
100+ self .try_commit (filepath , msg , user )
69101
70102 def delete (self , filepath : str = None ) -> None :
71103 super ().delete (filepath )
72- if filepath is not None :
104+ if self . repo is not None and filepath is not None :
73105 self .repo .index .add ([filepath ])
74106
75107 def move (self , src , dest ):
76108 super ().move (src , dest )
77- self .repo .index .add ([src , dst ])
109+ if self .repo is not None :
110+ self .repo .index .add ([src , dst ])
78111
79112 def copy_to (self , src_disk , dest = None ):
80113 super ().copy_to (src_disk , dest )
81- if dest is None :
114+ if dest is None and self . repo is not None :
82115 self .repo .index .add ([self .prefix ])
83116
84117 def distribute (self , filepath , allow_folders = True ):
0 commit comments