Coverage for pyquickhelper/loghelper/repositories/pygit_helper.py: 88%

286 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2023-06-03 02:21 +0200

1# -*- coding: utf-8 -*- 

2""" 

3@file 

4@brief Uses git to get version number. 

5""" 

6 

7import os 

8import sys 

9import datetime 

10import warnings 

11import xml.etree.ElementTree as ET 

12import re 

13from xml.sax.saxutils import escape 

14 

15from ..flog import fLOG, run_cmd 

16 

17 

18class GitException(Exception): 

19 """ 

20 Exception raised by this module. 

21 """ 

22 pass 

23 

24 

25def my_date_conversion(sdate): 

26 """ 

27 Converts a date into a datetime. 

28 

29 @param sdate string 

30 @return date 

31 """ 

32 first = sdate.split(" ")[0] 

33 trois = first.replace(".", "-").replace("/", "-").split("-") 

34 return datetime.datetime(int(trois[0]), int(trois[1]), int(trois[2])) 

35 

36 

37def IsRepo(location, commandline=True): 

38 """ 

39 Says if it a repository :epkg:`GIT`. 

40 

41 @param location (str) location 

42 @param commandline (bool) use commandline or not 

43 @return bool 

44 """ 

45 if location is None: 

46 location = os.path.normpath(os.path.abspath( 

47 os.path.join(os.path.split(__file__)[0], "..", "..", "..", ".."))) 

48 

49 try: 

50 get_repo_version(location, commandline, log=False) 

51 return True 

52 except Exception: 

53 return False 

54 

55 

56class RepoFile: 

57 

58 """ 

59 Mimic a :epkg:`GIT` file. 

60 """ 

61 

62 def __init__(self, **args): 

63 """ 

64 @param args list of members to add 

65 """ 

66 for k, v in args.items(): 

67 self.__dict__[k] = v 

68 

69 if hasattr(self, "name"): 

70 if '"' in self.name: # pylint: disable=E0203 

71 # defa = sys.stdout.encoding if sys.stdout != None else "utf8" 

72 self.name = self.name.replace('"', "") 

73 # self.name = self.name.encode(defa).decode("utf-8") 

74 if "\\303" in self.name or "\\302" in self.name or "\\342" in self.name: 

75 # don't know yet how to avoid that 

76 name0 = self.name 

77 # see http://www.utf8-chartable.de/unicode-utf8-table.pl?utf8=oct 

78 # far from perfect 

79 self.name = self.name.replace(r"\302\240", chr(160)) \ 

80 .replace(r"\302\246", "¦") \ 

81 .replace(r"\302\256", "®") \ 

82 .replace(r"\302\251", "©") \ 

83 .replace(r"\302\260", "°") \ 

84 .replace(r"\302\267", "·") \ 

85 .replace(r"\303\203", "Ã") \ 

86 .replace(r"\303\207", "Ç") \ 

87 .replace(r"\303\211", "e") \ 

88 .replace(r"\303\232", "Ú") \ 

89 .replace(r"\303\240", "à") \ 

90 .replace(r"\303\242", "â") \ 

91 .replace(r"\303\244", "ä") \ 

92 .replace(r"\303\246", "æ") \ 

93 .replace(r"\303\247", chr(231)) \ 

94 .replace(r"\303\250", chr(232)) \ 

95 .replace(r"\303\251", chr(233)) \ 

96 .replace(r"\303\252", "ê") \ 

97 .replace(r"\303\253", "ë") \ 

98 .replace(r"\303\256", "î") \ 

99 .replace(r"\303\257", "ï") \ 

100 .replace(r"\303\264", "ô") \ 

101 .replace(r"\303\266", "ö") \ 

102 .replace(r"\303\273", "û") \ 

103 .replace(r"\303\274", "ü") \ 

104 .replace(r"a\314\200", "à") \ 

105 .replace(r"e\314\201", "é") \ 

106 .replace(r"\342\200\231", "’") 

107 if not os.path.exists(self.name): 

108 try: 

109 ex = os.path.exists(name0) 

110 except ValueError as e: 

111 ex = str(e) 

112 warnings.warn( 

113 "The modification did not work\n'{0}'\nINTO\n'{1}'\n[{2}\nexists: {3}]".format( 

114 name0, self.name, [self.name], ex)) 

115 

116 def __str__(self): 

117 """ 

118 usual 

119 """ 

120 return self.name 

121 

122 

123def get_cmd_git(): 

124 """ 

125 Gets the command line used to run :epkg:`git`. 

126 

127 @return string 

128 """ 

129 if sys.platform.startswith("win32"): # pragma: no cover 

130 cmd = r'"C:\Program Files\Git\bin\git.exe"' 

131 if not os.path.exists(cmd): 

132 cmd = r'"C:\Program Files (x86)\Git\bin\git.exe"' 

133 if not os.path.exists(cmd): 

134 # hoping git path is included in environment variable PATH 

135 cmd = "git" 

136 else: 

137 cmd = 'git' 

138 return cmd 

139 

140 

141def repo_ls(full, commandline=True): 

142 """ 

143 Runs ``ls`` on a path. 

144 

145 @param full full path 

146 @param commandline use command line instead of pysvn 

147 @return output of client.ls 

148 """ 

149 

150 if not commandline: # pragma: no cover 

151 try: 

152 raise NotImplementedError() 

153 except Exception: 

154 return repo_ls(full, True) 

155 else: 

156 cmd = get_cmd_git() 

157 cmd += f" ls-tree -r HEAD \"{full}\"" 

158 out, err = run_cmd(cmd, 

159 wait=True, 

160 encerror="strict", 

161 encoding=sys.stdout.encoding if sys.stdout is not None else "utf8", 

162 change_path=os.path.split( 

163 full)[0] if os.path.isfile(full) else full, 

164 shell=sys.platform.startswith("win32")) 

165 if len(err) > 0: 

166 raise GitException( # pragma: no cover 

167 f"Issue with path '{full}'\n[OUT]\n{out}\n[ERR]\n{err}") 

168 

169 res = [RepoFile(name=os.path.join(full, _.strip().split("\t")[-1])) 

170 for _ in out.split("\n") if len(_) > 0] 

171 return res 

172 

173 

174def __get_version_from_version_txt(path): 

175 """ 

176 Private function, tries to find a file ``version.txt`` which should 

177 contains the version number (if :epkg:`svn` is not present). 

178 

179 @param path folder to look, it will look to the the path of this file, 

180 some parents directories and finally this path 

181 @return the version number 

182 

183 @warning If ``version.txt`` was not found, it throws an exception. 

184 """ 

185 file = os.path.split(__file__)[0] 

186 paths = [file, 

187 os.path.join(file, ".."), 

188 os.path.join(file, "..", ".."), 

189 os.path.join(file, "..", "..", ".."), 

190 path] 

191 for p in paths: 

192 fp = os.path.join(p, "version.txt") 

193 if os.path.exists(fp): 

194 with open(fp, "r") as f: 

195 return int(f.read().strip(" \n\r\t")) 

196 raise FileNotFoundError( 

197 "unable to find version.txt in\n" + "\n".join(paths)) 

198 

199 

200_reg_insertion = re.compile("([1-9][0-9]*) insertion") 

201_reg_deletion = re.compile("([1-9][0-9]*) deletion") 

202_reg_bytes = re.compile("([1-9][0-9]*) bytes") 

203 

204 

205def get_file_details(name, path=None, commandline=True): 

206 """ 

207 Returns information about a file. 

208 

209 @param name name of the file 

210 @param path path to repo 

211 @param commandline if True, use the command line to get the version number, otherwise it uses pysvn 

212 @return list of tuples 

213 

214 The result is a list of tuple: 

215 

216 * commit 

217 * name 

218 * added 

219 * inserted 

220 * bytes 

221 """ 

222 if not commandline: # pragma: no cover 

223 try: 

224 raise NotImplementedError() 

225 except Exception: 

226 return get_file_details(name, path, True) 

227 else: 

228 cmd = get_cmd_git() 

229 if sys.platform.startswith("win"): 

230 cmd += ' log --stat "' + os.path.join(path, name) + '"' 

231 else: 

232 cmd = [cmd, 'log', "--stat", os.path.join(path, name)] 

233 

234 enc = sys.stdout.encoding if sys.stdout is not None else "utf8" 

235 out, err = run_cmd(cmd, 

236 wait=True, 

237 encerror="strict", 

238 encoding=enc, 

239 change_path=os.path.split( 

240 path)[0] if os.path.isfile(path) else path, 

241 shell=sys.platform.startswith("win32"), 

242 preprocess=False) 

243 

244 if len(err) > 0: # pragma: no cover 

245 mes = f"Problem with file '{os.path.join(path, name)}'" 

246 raise GitException( 

247 mes + "\n" + 

248 err + "\nCMD:\n" + cmd + "\nOUT:\n" + out + "\n[giterror]\n" + 

249 err + "\nCMD:\n" + cmd) 

250 

251 master = get_master_location(path, commandline) 

252 if master.endswith(".git"): 

253 master = master[:-4] 

254 

255 if enc != "utf8" and enc is not None: 

256 by = out.encode(enc) 

257 out = by.decode("utf8") 

258 

259 # We split into commits. 

260 commits = [] 

261 current = [] 

262 for line in out.split("\n"): 

263 if line.startswith("commit"): 

264 if len(current) > 0: 

265 commits.append("\n".join(current)) 

266 current = [line] 

267 else: 

268 current.append(line) 

269 if len(current) > 0: 

270 commits.append("\n".join(current)) 

271 

272 # We analyze each commit. 

273 rows = [] 

274 for commit in commits: 

275 se = _reg_insertion.findall(commit) 

276 if len(se) > 1: 

277 raise RuntimeError( # pragma: no cover 

278 f"A commit is wrong \n{commit}") 

279 inser = int(se[0]) if len(se) == 1 else 0 

280 de = _reg_deletion.findall(commit) 

281 if len(de) > 1: 

282 raise RuntimeError( # pragma: no cover 

283 f"A commit is wrong \n{commit}") 

284 delet = int(de[0]) if len(de) == 1 else 0 

285 bi = _reg_bytes.findall(commit) 

286 if len(bi) > 1: 

287 raise RuntimeError( # pragma: no cover 

288 f"A commit is wrong \n{commit}") 

289 bite = int(bi[0]) if len(bi) == 1 else 0 

290 com = commit.split("\n")[0].split()[1] 

291 rows.append((com, name.strip(), inser, delet, bite)) 

292 return rows 

293 

294 

295_reg_stat_net = re.compile("(.+) *[|] +([1-9][0-9]*)") 

296_reg_stat_bytes = re.compile( 

297 "(.+) *[|] Bin ([0-9]+) [-][>] ([0-9]+) bytes") 

298 

299 

300def get_file_details_all(path=None, commandline=True): 

301 """ 

302 Returns information about all files 

303 

304 @param path path to repo 

305 @param commandline if True, use the command line to get the version number, otherwise it uses pysvn 

306 @return list of tuples 

307 

308 The result is a list of tuple: 

309 

310 * commit 

311 * name 

312 * net 

313 * bytes 

314 """ 

315 if not commandline: # pragma: no cover 

316 try: 

317 raise NotImplementedError() 

318 except Exception: 

319 return get_file_details_all(path, True) 

320 else: 

321 cmd = get_cmd_git() 

322 if sys.platform.startswith("win"): 

323 cmd += ' --no-pager log --stat' 

324 else: 

325 cmd = [cmd, '--no-pager', 'log', "--stat"] 

326 

327 enc = sys.stdout.encoding if sys.stdout is not None else "utf8" 

328 out, err = run_cmd(cmd, 

329 wait=True, 

330 encerror="strict", 

331 encoding=enc, 

332 change_path=os.path.split( 

333 path)[0] if os.path.isfile(path) else path, 

334 shell=sys.platform.startswith("win32"), 

335 preprocess=False) 

336 

337 if len(err) > 0: # pragma: no cover 

338 mes = f"Problem with '{path}'" 

339 raise GitException( 

340 mes + "\n" + 

341 err + "\nCMD:\n" + cmd + "\nOUT:\n" + out + "\n[giterror]\n" + 

342 err + "\nCMD:\n" + cmd) 

343 

344 master = get_master_location(path, commandline) 

345 if master.endswith(".git"): 

346 master = master[:-4] 

347 

348 if enc != "utf8" and enc is not None: 

349 by = out.encode(enc) 

350 out = by.decode("utf8") 

351 

352 # We split into commits. 

353 commits = [] 

354 current = [] 

355 for line in out.split("\n"): 

356 if line.startswith("commit"): 

357 if len(current) > 0: 

358 commits.append("\n".join(current)) 

359 current = [line] 

360 else: 

361 current.append(line) 

362 if len(current) > 0: 

363 commits.append("\n".join(current)) 

364 

365 # We analyze each commit. 

366 rows = [] 

367 for commit in commits: 

368 com = commit.split("\n")[0].split()[1] 

369 lines = commit.split("\n") 

370 for line in lines: 

371 r1 = _reg_stat_net.search(line) 

372 if r1: 

373 name = r1.groups()[0].strip() 

374 net = int(r1.groups()[1]) 

375 delta = 0 

376 else: 

377 net = 0 

378 r2 = _reg_stat_bytes.search(line) 

379 if r2: 

380 name = r2.groups()[0].strip() 

381 fr = int(r2.groups()[1]) 

382 to = int(r2.groups()[2]) 

383 delta = to - fr 

384 else: 

385 continue 

386 rows.append((com, name, net, delta)) 

387 return rows 

388 

389 

390def get_repo_log(path=None, file_detail=False, commandline=True, subset=None): 

391 """ 

392 Gets the latest changes operated on a file in a folder or a subfolder. 

393 

394 @param path path to look 

395 @param file_detail if True, add impacted files 

396 @param commandline if True, use the command line to get the version number, otherwise it uses pysvn 

397 @param subset only provide file details for a subset of files 

398 @return list of changes, each change is a list of tuple (see below) 

399 

400 The return results is a list of tuple with the following fields: 

401 

402 - author 

403 - commit hash [:6] 

404 - date (datetime) 

405 - comment$ 

406 - full commit hash 

407 - link to commit (if the repository is http://...) 

408 

409 The function use a command line if an error occurred. 

410 It uses the xml format: 

411 

412 :: 

413 

414 <logentry revision="161"> 

415 <author>xavier dupre</author> 

416 <date>2013-03-23T15:02:50.311828Z</date> 

417 <msg>pyquickhelper: first version</msg> 

418 <hash>full commit hash</hash> 

419 </logentry> 

420 

421 Add link: 

422 

423 :: 

424 

425 https://github.com/sdpython/pyquickhelper/commit/8d5351d1edd4a8997f358be39da80c72b06c2272 

426 

427 More: `git pretty format <http://opensource.apple.com/source/Git/Git-19/src/git-htmldocs/pretty-formats.txt>`_ 

428 See also `pretty format <https://www.kernel.org/pub/software/scm/git/docs/git-log.html#_pretty_formats>`_ (html). 

429 To get details about one file and all the commit. 

430 

431 :: 

432 

433 git log --stat -- _unittests/ut_loghelper/data/sample_zip.zip 

434 

435 For some reason, the call to @see fn str2datetime seemed to cause exception such as:: 

436 

437 File "<frozen importlib._bootstrap>", line 2212, in _find_and_load_unlocked 

438 File "<frozen importlib._bootstrap>", line 321, in _call_with_frames_removed 

439 File "<frozen importlib._bootstrap>", line 2254, in _gcd_import 

440 File "<frozen importlib._bootstrap>", line 2237, in _find_and_load 

441 File "<frozen importlib._bootstrap>", line 2224, in _find_and_load_unlocked 

442 

443 when it was used to generate documentation for others modules than *pyquickhelper*. 

444 Not using this function helps. The cause still remains obscure. 

445 """ 

446 if file_detail: 

447 if subset is None: 

448 res = get_file_details_all(path, commandline=commandline) 

449 details = {} 

450 for commit in res: 

451 com = commit[0] 

452 if com not in details: 

453 details[com] = [] 

454 details[com].append(commit[1:]) 

455 else: 

456 files = subset 

457 details = {} 

458 for i, name in enumerate(files): 

459 res = get_file_details(name.name if isinstance(name, RepoFile) else name, 

460 path, commandline=commandline) 

461 for commit in res: 

462 com = commit[0] 

463 if com not in details: 

464 details[com] = [] 

465 details[com].append(commit[1:]) 

466 logs = get_repo_log(path=path, file_detail=False, 

467 commandline=commandline) 

468 final = [] 

469 for log in logs: 

470 com = log[4] 

471 if com not in details: 

472 continue 

473 det = details[com] 

474 for d in det: 

475 final.append(tuple(log) + d) 

476 return final 

477 

478 if path is None: 

479 path = os.path.normpath( 

480 os.path.abspath(os.path.join(os.path.split(__file__)[0], "..", "..", ".."))) 

481 

482 if not commandline: # pragma: no cover 

483 try: 

484 raise NotImplementedError() 

485 except Exception: 

486 return get_repo_log(path, file_detail, True) 

487 else: 

488 cmd = get_cmd_git() 

489 if sys.platform.startswith("win"): # pragma: no cover 

490 cmd += ' log --pretty=format:"<logentry revision=\\"%h\\">' + \ 

491 '<author>%an</author><date>%ci</date><hash>%H</hash><msg>%s</msg></logentry>" ' + \ 

492 path 

493 else: 

494 cmd_tmp = '--pretty=format:<logentry revision="%h"><author>%an</author><date>%ci' + \ 

495 '</date><hash>%H</hash><msg>%s</msg></logentry>' 

496 cmd = [cmd, 'log', cmd_tmp, path] 

497 

498 enc = sys.stdout.encoding if sys.stdout is not None else "utf8" 

499 out, err = run_cmd(cmd, wait=True, encerror="strict", encoding=enc, 

500 change_path=os.path.split( 

501 path)[0] if os.path.isfile(path) else path, 

502 shell=sys.platform.startswith("win32"), preprocess=False) 

503 

504 if len(err) > 0: # pragma: no cover 

505 mes = f"Problem with file '{path}'" 

506 raise GitException(mes + "\n" + 

507 err + "\nCMD:\n" + cmd + "\nOUT:\n" + out + 

508 "\n[giterror]\n" + err + "\nCMD:\n" + cmd) 

509 

510 master = get_master_location(path, commandline) 

511 if master.endswith(".git"): 

512 master = master[:-4] 

513 

514 if enc != "utf8" and enc is not None: 

515 by = out.encode(enc) 

516 out = by.decode("utf8") 

517 

518 out = out.replace("\n\n", "\n") 

519 out = f"<xml>\n{out}\n</xml>" 

520 try: 

521 root = ET.fromstring(out) 

522 except ET.ParseError: 

523 # it might be due to character such as << >> 

524 lines = out.split("\n") 

525 out = [] 

526 suffix = "</msg></logentry>" 

527 for line in lines: 

528 if line.endswith(suffix): 

529 pos = line.find("<msg>") 

530 if pos == -1: 

531 out.append(line) 

532 continue 

533 begin = line[:pos + 5] 

534 body = line[pos + 5:-len(suffix)] 

535 msg = escape(body) 

536 line = begin + msg + suffix 

537 out.append(line) 

538 out = "\n".join(out) 

539 try: 

540 root = ET.fromstring(out) 

541 except ET.ParseError as eee: # pragma: no cover 

542 raise GitException( 

543 f"Unable to parse:\n{out}") from eee 

544 

545 res = [] 

546 for i in root.iter('logentry'): 

547 revision = i.attrib['revision'].strip() 

548 author = i.find("author").text.strip() 

549 t = i.find("msg").text 

550 hash = i.find("hash").text 

551 msg = t.strip() if t is not None else "-" 

552 sdate = i.find("date").text.strip() 

553 dt = my_date_conversion(sdate.replace("T", " ").strip("Z ")) 

554 row = [author, revision, dt, msg, hash] 

555 if master.startswith("http"): 

556 row.append(master + "/commit/" + hash) 

557 else: 

558 row.append(f"{master}//{hash}") 

559 res.append(row) 

560 return res 

561 

562 

563def get_repo_version(path=None, commandline=True, usedate=False, log=False): 

564 """ 

565 Gets the latest check for a specific path or version number 

566 based on the date (if *usedate* is True). 

567 If *usedate* is False, it returns a mini hash (a string then). 

568 

569 @param path path to look 

570 @param commandline if True, use the command line to get the version number, otherwise it uses pysvn 

571 @param usedate if True, it uses the date to return a minor version number (1.1.thisone) 

572 @param log if True, returns the output instead of a boolean 

573 @return integer) 

574 """ 

575 if not usedate: 

576 last = get_nb_commits(path, commandline) 

577 return last 

578 else: # pragma: no cover 

579 if path is None: 

580 path = os.path.normpath( 

581 os.path.abspath(os.path.join(os.path.split(__file__)[0], "..", "..", ".."))) 

582 

583 if not commandline: 

584 try: 

585 raise NotImplementedError() 

586 except Exception: 

587 return get_repo_version(path, True) 

588 else: 

589 cmd = get_cmd_git() 

590 cmd += ' git log --format="%h---%ci"' 

591 

592 if path is not None: 

593 cmd += f" \"{path}\"" 

594 

595 try: 

596 out, err = run_cmd(cmd, wait=True, encerror="strict", 

597 encoding=sys.stdout.encoding if sys.stdout is not None else "utf8", 

598 change_path=os.path.split( 

599 path)[0] if os.path.isfile(path) else path, 

600 log_error=False, shell=sys.platform.startswith("win32")) 

601 except Exception as e: 

602 raise GitException( 

603 f"Problem with subprocess. Path is '{path}'\n[CMD]\n{cmd}") from e 

604 

605 if len(err) > 0: 

606 if log: 

607 fLOG("Problem with file ", path, err) 

608 if log: 

609 return f"OUT\n{out}\n[giterror]{err}\nCMD:\n{cmd}" 

610 else: 

611 raise GitException( 

612 f"OUT\n{out}\n[giterror]{err}\nCMD:\n{cmd}") 

613 

614 lines = out.split("\n") 

615 lines = [_.split("---") for _ in lines if len(_) > 0] 

616 temp = lines[0] 

617 if usedate: 

618 dt = my_date_conversion(temp[1].replace("T", " ").strip("Z ")) 

619 dt0 = datetime.datetime(dt.year, 1, 1, 0, 0, 0) 

620 res = "%d" % (dt - dt0).days 

621 else: 

622 res = temp[0] 

623 

624 if len(res) == 0: 

625 raise GitException( 

626 "The command 'git help' should return something.") 

627 

628 return res 

629 

630 

631def get_master_location(path=None, commandline=True): 

632 """ 

633 Gets the remote master location. 

634 

635 @param path path to look 

636 @param commandline if True, use the command line to get the version number, otherwise it uses pysvn 

637 @return integer (check in number) 

638 """ 

639 if path is None: 

640 path = os.path.normpath( 

641 os.path.abspath(os.path.join(os.path.split(__file__)[0], "..", "..", ".."))) 

642 

643 if not commandline: # pragma: no cover 

644 try: 

645 raise NotImplementedError() 

646 except Exception: 

647 return get_master_location(path, True) 

648 else: 

649 cmd = get_cmd_git() 

650 cmd += " config --get remote.origin.url" 

651 

652 try: 

653 out, err = run_cmd(cmd, wait=True, encerror="strict", 

654 encoding=sys.stdout.encoding if sys.stdout is not None else "utf8", 

655 change_path=os.path.split( 

656 path)[0] if os.path.isfile(path) else path, 

657 log_error=False, shell=sys.platform.startswith("win32")) 

658 except Exception as e: # pragma: no cover 

659 raise GitException( 

660 f"Problem with subprocess. Path is '{path}'\n[CMD]\n{cmd}") from e 

661 

662 if len(err) > 0: 

663 raise GitException( # pragma: no cover 

664 f"Problem with path '{path}'\n[OUT]\n{out}\n[ERR]\n{err}") 

665 lines = out.split("\n") 

666 lines = [_ for _ in lines if len(_) > 0] 

667 res = lines[0] 

668 

669 if len(res) == 0: 

670 raise GitException( # pragma: no cover 

671 "The command 'git help' should return something.") 

672 

673 return res 

674 

675 

676def get_nb_commits(path=None, commandline=True): 

677 """ 

678 Returns the number of commit. 

679 

680 @param path path to look 

681 @param commandline if True, use the command line to get the version number, otherwise it uses pysvn 

682 @return integer 

683 """ 

684 if path is None: 

685 path = os.path.normpath( 

686 os.path.abspath(os.path.join(os.path.split(__file__)[0], "..", "..", ".."))) 

687 

688 if not commandline: # pragma: no cover 

689 try: 

690 raise NotImplementedError() 

691 except Exception: 

692 return get_nb_commits(path, True) 

693 else: 

694 cmd = get_cmd_git() 

695 cmd += ' rev-list HEAD --count' 

696 

697 if path is not None: 

698 cmd += f" \"{path}\"" 

699 

700 out, err = run_cmd(cmd, 

701 wait=True, 

702 encerror="strict", 

703 encoding=sys.stdout.encoding if sys.stdout is not None else "utf8", 

704 change_path=os.path.split( 

705 path)[0] if os.path.isfile(path) else path, 

706 log_error=False, 

707 shell=sys.platform.startswith("win32")) 

708 

709 if len(err) > 0: 

710 raise GitException( # pragma: no cover 

711 f"Unable to get commit number from path {path}\n[giterror]\n{err}\nCMD:\n{cmd}") 

712 

713 lines = out.strip() 

714 try: 

715 nb = int(lines) 

716 except ValueError as e: 

717 raise ValueError( # pragma: no cover 

718 "unable to parse: " + lines + "\nCMD:\n" + cmd) from e 

719 return nb 

720 

721 

722def get_file_last_modification(path, commandline=True): 

723 """ 

724 Returns the last modification of a file. 

725 

726 @param path path to look 

727 @param commandline if True, use the command line to get the version number, otherwise it uses pysvn 

728 @return integer 

729 """ 

730 if path is None: 

731 path = os.path.normpath( 

732 os.path.abspath(os.path.join(os.path.split(__file__)[0], "..", "..", ".."))) 

733 

734 if not commandline: # pragma: no cover 

735 try: 

736 raise NotImplementedError() 

737 except Exception: 

738 return get_file_last_modification(path, True) 

739 else: 

740 cmd = get_cmd_git() 

741 cmd += ' log -1 --format="%ad" --' 

742 cmd += f" \"{path}\"" 

743 

744 out, err = run_cmd(cmd, 

745 wait=True, 

746 encerror="strict", 

747 encoding=sys.stdout.encoding if sys.stdout is not None else "utf8", 

748 change_path=os.path.split( 

749 path)[0] if os.path.isfile(path) else path, 

750 log_error=False, 

751 shell=sys.platform.startswith("win32")) 

752 

753 if len(err) > 0: 

754 raise GitException( # pragma: no cover 

755 f"Unable to get commit number from path {path}\n[giterror]\n{err}\nCMD:\n{cmd}") 

756 

757 lines = out.strip("\n\r ") 

758 return lines 

759 

760 

761def clone(location, srv, group, project, username=None, password=None, fLOG=None): 

762 """ 

763 Clones a :epkg:`git` repository. 

764 

765 @param location location of the clone 

766 @param srv git server 

767 @param group group 

768 @param project project name 

769 @param username username 

770 @param password password 

771 @param fLOG logging function 

772 @return output, error 

773 

774 See `How to provide username and password when run "git clone git@remote.git"? 

775 <http://stackoverflow.com/questions/10054318/how-to-provide-username-and-password-when-run-git-clone-gitremote-git>`_ 

776 

777 .. exref:: 

778 :title: Clone a git repository 

779 

780 :: 

781 

782 clone("local_folder", "github.com", "sdpython", "pyquickhelper") 

783 """ 

784 if username is not None: 

785 address = f"https://{username}:{password}@{srv}/{group}/{project}.git" 

786 else: 

787 address = f"https://{srv}/{group}/{project}.git" 

788 

789 cmd = get_cmd_git() 

790 cmd += " clone " + address + " " + location 

791 out, err = run_cmd(cmd, wait=True, fLOG=fLOG) 

792 if len(err) > 0 and "Cloning into" not in err and "Clonage dans" not in err: 

793 raise GitException( # pragma: no cover 

794 f"Unable to clone {address}\n[giterror]\n{err}\nCMD:\n{cmd}") 

795 return out, err 

796 

797 

798def rebase(location, srv, group, project, username=None, password=None, fLOG=None): 

799 """ 

800 Runs ``git pull -rebase`` on a repository. 

801 

802 @param location location of the clone 

803 @param srv git server 

804 @param group group 

805 @param project project name 

806 @param username username 

807 @param password password 

808 @param fLOG logging function 

809 @return output, error 

810 """ 

811 if username is not None: 

812 address = f"https://{username}:{password}@{srv}/{group}/{project}.git" 

813 else: 

814 address = f"https://{srv}/{group}/{project}.git" 

815 

816 cwd = os.getcwd() 

817 os.chdir(location) 

818 cmd = get_cmd_git() 

819 cmd += " pull --rebase " + address 

820 out, err = run_cmd(cmd, wait=True, fLOG=fLOG) 

821 os.chdir(cwd) 

822 if len(err) > 0 and "-> FETCH_HEAD" not in err: 

823 raise GitException( # pragma: no cover 

824 f"Unable to rebase {address}\n[giterror]\n{err}\nCMD:\n{cmd}") 

825 return out, err