4
4
import os
5
5
import subprocess
6
6
from subprocess import Popen , PIPE , CalledProcessError
7
- from urllib .parse import unquote
8
7
8
+ import pexpect
9
+ from urllib .parse import unquote
9
10
from tornado .web import HTTPError
10
11
11
12
12
13
ALLOWED_OPTIONS = ['user.name' , 'user.email' ]
13
14
14
15
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
+
15
62
class Git :
16
63
"""
17
64
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):
110
157
return response
111
158
112
159
113
- def clone (self , current_path , repo_url ):
160
+ def clone (self , current_path , repo_url , auth = None ):
114
161
"""
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
116
165
:param current_path: the directory where the clone will be performed.
117
166
:param repo_url: the URL of the repository to be cloned.
167
+ :param auth: OPTIONAL dictionary with 'username' and 'password' fields
118
168
:return: response with status code and error message.
119
169
"""
120
170
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 ()
130
191
131
192
response = {"code" : p .returncode }
132
193
@@ -544,22 +605,33 @@ def commit(self, commit_msg, top_repo_path):
544
605
["git" , "commit" , "-m" , commit_msg ], cwd = top_repo_path
545
606
)
546
607
return my_output
547
-
548
- def pull (self , curr_fb_path ):
608
+
609
+ def pull (self , curr_fb_path , auth = None ):
549
610
"""
550
611
Execute git pull --no-commit. Disables prompts for the password to avoid the terminal hanging while waiting
551
612
for auth.
552
613
"""
553
614
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 ()
563
635
564
636
response = {"code" : p .returncode }
565
637
@@ -568,20 +640,31 @@ def pull(self, curr_fb_path):
568
640
569
641
return response
570
642
571
- def push (self , remote , branch , curr_fb_path ):
643
+ def push (self , remote , branch , curr_fb_path , auth = None ):
572
644
"""
573
645
Execute `git push $UPSTREAM $BRANCH`. The choice of upstream and branch is up to the caller.
574
646
"""
575
647
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 ()
585
668
586
669
response = {"code" : p .returncode }
587
670
0 commit comments