44import os
55import subprocess
66from subprocess import Popen , PIPE , CalledProcessError
7- from urllib .parse import unquote
87
8+ import pexpect
9+ from urllib .parse import unquote
910from tornado .web import HTTPError
1011
1112
1213ALLOWED_OPTIONS = ['user.name' , 'user.email' ]
1314
1415
16+ class GitAuthInputWrapper :
17+ """
18+ Helper class which is meant to replace subprocess.Popen for communicating
19+ with git CLI when also sending username and password for auth
20+ """
21+ def __init__ (self , command , cwd , env , username , password ):
22+ self .command = command
23+ self .cwd = cwd
24+ self .env = env
25+ self .username = username
26+ self .password = password
27+ def communicate (self ):
28+ try :
29+ p = pexpect .spawn (
30+ self .command ,
31+ cwd = self .cwd ,
32+ env = self .env
33+ )
34+
35+ # We expect a prompt from git
36+ # In most of cases git will prompt for username and
37+ # then for password
38+ # In some cases (Bitbucket) username is included in
39+ # remote URL, so git will not ask for username
40+ i = p .expect (['Username for .*: ' , 'Password for .*:' ])
41+ if i == 0 : #ask for username then password
42+ p .sendline (self .username )
43+ p .expect ('Password for .*:' )
44+ p .sendline (self .password )
45+ elif i == 1 : #only ask for password
46+ p .sendline (self .password )
47+
48+ p .expect (pexpect .EOF )
49+ response = p .before
50+
51+ self .returncode = p .wait ()
52+ p .close ()
53+
54+ return response
55+ except pexpect .exceptions .EOF : #In case of pexpect failure
56+ response = p .before
57+ self .returncode = p .exitstatus
58+ p .close () #close process
59+ return response
60+
61+
1562class Git :
1663 """
1764 A single parent class containing all of the individual git methods in it.
@@ -110,23 +157,37 @@ def changed_files(self, base=None, remote=None, single_commit=None):
110157 return response
111158
112159
113- def clone (self , current_path , repo_url ):
160+ def clone (self , current_path , repo_url , auth = None ):
114161 """
115- Execute `git clone`. Disables prompts for the password to avoid the terminal hanging.
162+ Execute `git clone`.
163+ When no auth is provided, disables prompts for the password to avoid the terminal hanging.
164+ When auth is provided, await prompts for username/passwords and sends them
116165 :param current_path: the directory where the clone will be performed.
117166 :param repo_url: the URL of the repository to be cloned.
167+ :param auth: OPTIONAL dictionary with 'username' and 'password' fields
118168 :return: response with status code and error message.
119169 """
120170 env = os .environ .copy ()
121- env ["GIT_TERMINAL_PROMPT" ] = "0"
122- p = subprocess .Popen (
123- ["git" , "clone" , unquote (repo_url )],
124- stdout = PIPE ,
125- stderr = PIPE ,
126- cwd = os .path .join (self .root_dir , current_path ),
127- env = env ,
128- )
129- _ , error = p .communicate ()
171+ if (auth ):
172+ env ["GIT_TERMINAL_PROMPT" ] = "1"
173+ p = GitAuthInputWrapper (
174+ command = 'git clone {} -q' .format (unquote (repo_url )),
175+ cwd = os .path .join (self .root_dir , current_path ),
176+ env = env ,
177+ username = auth ['username' ],
178+ password = auth ['password' ],
179+ )
180+ error = p .communicate ()
181+ else :
182+ env ["GIT_TERMINAL_PROMPT" ] = "0"
183+ p = subprocess .Popen (
184+ ['git' , 'clone' , unquote (repo_url )],
185+ stdout = PIPE ,
186+ stderr = PIPE ,
187+ env = env ,
188+ cwd = os .path .join (self .root_dir , current_path ),
189+ )
190+ _ , error = p .communicate ()
130191
131192 response = {"code" : p .returncode }
132193
@@ -544,22 +605,33 @@ def commit(self, commit_msg, top_repo_path):
544605 ["git" , "commit" , "-m" , commit_msg ], cwd = top_repo_path
545606 )
546607 return my_output
547-
548- def pull (self , curr_fb_path ):
608+
609+ def pull (self , curr_fb_path , auth = None ):
549610 """
550611 Execute git pull --no-commit. Disables prompts for the password to avoid the terminal hanging while waiting
551612 for auth.
552613 """
553614 env = os .environ .copy ()
554- env ["GIT_TERMINAL_PROMPT" ] = "0"
555- p = subprocess .Popen (
556- ["git" , "pull" , "--no-commit" ],
557- stdout = PIPE ,
558- stderr = PIPE ,
559- cwd = os .path .join (self .root_dir , curr_fb_path ),
560- env = env ,
561- )
562- _ , error = p .communicate ()
615+ if (auth ):
616+ env ["GIT_TERMINAL_PROMPT" ] = "1"
617+ p = GitAuthInputWrapper (
618+ command = 'git pull --no-commit' ,
619+ cwd = os .path .join (self .root_dir , curr_fb_path ),
620+ env = env ,
621+ username = auth ['username' ],
622+ password = auth ['password' ]
623+ )
624+ error = p .communicate ()
625+ else :
626+ env ["GIT_TERMINAL_PROMPT" ] = "0"
627+ p = subprocess .Popen (
628+ ['git' , 'pull' , '--no-commit' ],
629+ stdout = PIPE ,
630+ stderr = PIPE ,
631+ env = env ,
632+ cwd = os .path .join (self .root_dir , curr_fb_path ),
633+ )
634+ _ , error = p .communicate ()
563635
564636 response = {"code" : p .returncode }
565637
@@ -568,20 +640,31 @@ def pull(self, curr_fb_path):
568640
569641 return response
570642
571- def push (self , remote , branch , curr_fb_path ):
643+ def push (self , remote , branch , curr_fb_path , auth = None ):
572644 """
573645 Execute `git push $UPSTREAM $BRANCH`. The choice of upstream and branch is up to the caller.
574646 """
575647 env = os .environ .copy ()
576- env ["GIT_TERMINAL_PROMPT" ] = "0"
577- p = subprocess .Popen (
578- ["git" , "push" , remote , branch ],
579- stdout = PIPE ,
580- stderr = PIPE ,
581- cwd = os .path .join (self .root_dir , curr_fb_path ),
582- env = env ,
583- )
584- _ , error = p .communicate ()
648+ if (auth ):
649+ env ["GIT_TERMINAL_PROMPT" ] = "1"
650+ p = GitAuthInputWrapper (
651+ command = 'git push {} {}' .format (remote , branch ),
652+ cwd = os .path .join (self .root_dir , curr_fb_path ),
653+ env = env ,
654+ username = auth ['username' ],
655+ password = auth ['password' ]
656+ )
657+ error = p .communicate ()
658+ else :
659+ env ["GIT_TERMINAL_PROMPT" ] = "0"
660+ p = subprocess .Popen (
661+ ['git' , 'push' , remote , branch ],
662+ stdout = PIPE ,
663+ stderr = PIPE ,
664+ env = env ,
665+ cwd = os .path .join (self .root_dir , curr_fb_path ),
666+ )
667+ _ , error = p .communicate ()
585668
586669 response = {"code" : p .returncode }
587670
0 commit comments