|
9 | 9 | from pyinfra import host |
10 | 10 | from pyinfra.api import OperationError, QuoteString, StringCommand, operation |
11 | 11 | from pyinfra.facts.files import Directory, File |
12 | | -from pyinfra.facts.git import GitBranch, GitConfig, GitTag, GitTrackingBranch |
| 12 | +from pyinfra.facts.git import ( |
| 13 | + GitBranch, |
| 14 | + GitConfig, |
| 15 | + GitLocalCommit, |
| 16 | + GitRemoteBranchCommit, |
| 17 | + GitTag, |
| 18 | + GitTrackingBranch, |
| 19 | +) |
13 | 20 |
|
14 | 21 | from . import files, ssh |
15 | 22 | from .util.files import chown, unix_path_join |
@@ -154,14 +161,35 @@ def repo( |
154 | 161 | # Ensuring existing repo |
155 | 162 | else: |
156 | 163 | is_tag = False |
157 | | - if branch and host.get_fact(GitBranch, repo=dest) != branch: |
| 164 | + current_branch = host.get_fact(GitBranch, repo=dest) |
| 165 | + if branch is not None and current_branch != branch: |
158 | 166 | git_commands.append("fetch") # fetch to ensure we have the branch locally |
159 | 167 | git_commands.append(StringCommand("checkout", QuoteString(branch))) |
160 | 168 | if branch and branch in (host.get_fact(GitTag, repo=dest) or []): |
161 | 169 | git_commands.append(StringCommand("checkout", QuoteString(branch))) |
162 | 170 | is_tag = True |
163 | 171 | if pull and not is_tag: |
164 | | - if rebase: |
| 172 | + skip_pull = False |
| 173 | + # Skip `git pull` when the local branch tip already matches the |
| 174 | + # remote tip, so pyinfra reports the operation unchanged rather |
| 175 | + # than always "Success". This still applies when we switch branch: |
| 176 | + # if the target branch already exists locally at the remote tip, |
| 177 | + # the fetch+checkout leaves nothing for pull to do. |
| 178 | + effective_branch = branch or current_branch |
| 179 | + if effective_branch: |
| 180 | + local_commit = host.get_fact(GitLocalCommit, repo=dest, ref=effective_branch) |
| 181 | + remote_commit = host.get_fact( |
| 182 | + GitRemoteBranchCommit, |
| 183 | + repo=dest, |
| 184 | + branch=effective_branch, |
| 185 | + ) |
| 186 | + if local_commit and remote_commit and local_commit == remote_commit: |
| 187 | + skip_pull = True |
| 188 | + if skip_pull: |
| 189 | + host.noop( |
| 190 | + "git repository {0} is already up to date".format(dest), |
| 191 | + ) |
| 192 | + elif rebase: |
165 | 193 | git_commands.append("pull --rebase") |
166 | 194 | else: |
167 | 195 | git_commands.append("pull") |
@@ -389,31 +417,62 @@ def worktree( |
389 | 417 | # pull the worktree only if it's already linked to a tracking branch or |
390 | 418 | # if a remote branch is set |
391 | 419 | elif host.get_fact(GitTrackingBranch, repo=worktree) or from_remote_branch: |
392 | | - pull_args: list[str | QuoteString] = [ |
393 | | - "cd", |
394 | | - QuoteString(worktree), |
395 | | - "&&", |
396 | | - "git", |
397 | | - "pull", |
398 | | - ] |
399 | | - |
400 | | - if rebase: |
401 | | - pull_args.append("--rebase") |
| 420 | + if from_remote_branch and ( |
| 421 | + len(from_remote_branch) != 2 or type(from_remote_branch) not in (tuple, list) |
| 422 | + ): |
| 423 | + raise OperationError( |
| 424 | + "The remote branch must be a 2-tuple (remote, branch) such as " |
| 425 | + '("origin", "master")', |
| 426 | + ) |
402 | 427 |
|
| 428 | + # Determine the remote ref we would pull from, so we can short-circuit |
| 429 | + # the pull when the worktree HEAD already matches the remote tip. |
| 430 | + remote_name: str | None = None |
| 431 | + remote_branch: str | None = None |
403 | 432 | if from_remote_branch: |
404 | | - if len(from_remote_branch) != 2 or type(from_remote_branch) not in (tuple, list): |
405 | | - raise OperationError( |
406 | | - "The remote branch must be a 2-tuple (remote, branch) such as " |
407 | | - '("origin", "master")', |
408 | | - ) |
409 | | - pull_args.extend( |
410 | | - [ |
411 | | - QuoteString(from_remote_branch[0]), |
412 | | - QuoteString(from_remote_branch[1]), |
413 | | - ] |
| 433 | + remote_name, remote_branch = from_remote_branch[0], from_remote_branch[1] |
| 434 | + else: |
| 435 | + tracking = host.get_fact(GitTrackingBranch, repo=worktree) |
| 436 | + if tracking and "/" in tracking: |
| 437 | + remote_name, remote_branch = tracking.split("/", 1) |
| 438 | + |
| 439 | + skip_pull = False |
| 440 | + if remote_name and remote_branch: |
| 441 | + local_commit = host.get_fact(GitLocalCommit, repo=worktree) |
| 442 | + remote_commit = host.get_fact( |
| 443 | + GitRemoteBranchCommit, |
| 444 | + repo=worktree, |
| 445 | + remote=remote_name, |
| 446 | + branch=remote_branch, |
| 447 | + ) |
| 448 | + if local_commit and remote_commit and local_commit == remote_commit: |
| 449 | + skip_pull = True |
| 450 | + |
| 451 | + if skip_pull: |
| 452 | + host.noop( |
| 453 | + "git worktree {0} is already up to date".format(worktree), |
414 | 454 | ) |
| 455 | + else: |
| 456 | + pull_args: list[str | QuoteString] = [ |
| 457 | + "cd", |
| 458 | + QuoteString(worktree), |
| 459 | + "&&", |
| 460 | + "git", |
| 461 | + "pull", |
| 462 | + ] |
| 463 | + |
| 464 | + if rebase: |
| 465 | + pull_args.append("--rebase") |
| 466 | + |
| 467 | + if from_remote_branch: |
| 468 | + pull_args.extend( |
| 469 | + [ |
| 470 | + QuoteString(from_remote_branch[0]), |
| 471 | + QuoteString(from_remote_branch[1]), |
| 472 | + ] |
| 473 | + ) |
415 | 474 |
|
416 | | - yield StringCommand(*pull_args) |
| 475 | + yield StringCommand(*pull_args) |
417 | 476 |
|
418 | 477 |
|
419 | 478 | @operation() |
|
0 commit comments