git-nested 1.0.3__tar.gz → 1.0.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. {git_nested-1.0.3 → git_nested-1.0.4}/PKG-INFO +1 -1
  2. {git_nested-1.0.3 → git_nested-1.0.4}/lib/git_nested.egg-info/PKG-INFO +1 -1
  3. {git_nested-1.0.3 → git_nested-1.0.4}/lib/git_nested.py +68 -2
  4. {git_nested-1.0.3 → git_nested-1.0.4}/test/scenarios/test_filter.py +33 -0
  5. {git_nested-1.0.3 → git_nested-1.0.4}/.codecov.yml +0 -0
  6. {git_nested-1.0.3 → git_nested-1.0.4}/.fish.rc +0 -0
  7. {git_nested-1.0.3 → git_nested-1.0.4}/.gitattributes +0 -0
  8. {git_nested-1.0.3 → git_nested-1.0.4}/.github/workflows/publish.yml +0 -0
  9. {git_nested-1.0.3 → git_nested-1.0.4}/.github/workflows/test.yml +0 -0
  10. {git_nested-1.0.3 → git_nested-1.0.4}/.gitignore +0 -0
  11. {git_nested-1.0.3 → git_nested-1.0.4}/.rc +0 -0
  12. {git_nested-1.0.3 → git_nested-1.0.4}/CONTRIBUTING.md +0 -0
  13. {git_nested-1.0.3 → git_nested-1.0.4}/LICENSE +0 -0
  14. {git_nested-1.0.3 → git_nested-1.0.4}/README.md +0 -0
  15. {git_nested-1.0.3 → git_nested-1.0.4}/docs/diagrams.md +0 -0
  16. {git_nested-1.0.3 → git_nested-1.0.4}/lib/git-nested +0 -0
  17. {git_nested-1.0.3 → git_nested-1.0.4}/lib/git_nested.egg-info/SOURCES.txt +0 -0
  18. {git_nested-1.0.3 → git_nested-1.0.4}/lib/git_nested.egg-info/dependency_links.txt +0 -0
  19. {git_nested-1.0.3 → git_nested-1.0.4}/lib/git_nested.egg-info/entry_points.txt +0 -0
  20. {git_nested-1.0.3 → git_nested-1.0.4}/lib/git_nested.egg-info/requires.txt +0 -0
  21. {git_nested-1.0.3 → git_nested-1.0.4}/lib/git_nested.egg-info/top_level.txt +0 -0
  22. {git_nested-1.0.3 → git_nested-1.0.4}/pyproject.toml +0 -0
  23. {git_nested-1.0.3 → git_nested-1.0.4}/setup.cfg +0 -0
  24. {git_nested-1.0.3 → git_nested-1.0.4}/share/completion.bash +0 -0
  25. {git_nested-1.0.3 → git_nested-1.0.4}/share/enable-completion.sh +0 -0
  26. {git_nested-1.0.3 → git_nested-1.0.4}/share/git-nested.fish +0 -0
  27. {git_nested-1.0.3 → git_nested-1.0.4}/share/zsh-completion/_git-nested +0 -0
  28. {git_nested-1.0.3 → git_nested-1.0.4}/test/conftest.py +0 -0
  29. {git_nested-1.0.3 → git_nested-1.0.4}/test/scenarios/test_extraction.py +0 -0
  30. {git_nested-1.0.3 → git_nested-1.0.4}/test/scenarios/test_filemode.py +0 -0
  31. {git_nested-1.0.3 → git_nested-1.0.4}/test/test_branch.py +0 -0
  32. {git_nested-1.0.3 → git_nested-1.0.4}/test/test_clean.py +0 -0
  33. {git_nested-1.0.3 → git_nested-1.0.4}/test/test_clone.py +0 -0
  34. {git_nested-1.0.3 → git_nested-1.0.4}/test/test_completion.py +0 -0
  35. {git_nested-1.0.3 → git_nested-1.0.4}/test/test_encode.py +0 -0
  36. {git_nested-1.0.3 → git_nested-1.0.4}/test/test_error.py +0 -0
  37. {git_nested-1.0.3 → git_nested-1.0.4}/test/test_fetch.py +0 -0
  38. {git_nested-1.0.3 → git_nested-1.0.4}/test/test_gitignore.py +0 -0
  39. {git_nested-1.0.3 → git_nested-1.0.4}/test/test_init.py +0 -0
  40. {git_nested-1.0.3 → git_nested-1.0.4}/test/test_nested_in_nested.py +0 -0
  41. {git_nested-1.0.3 → git_nested-1.0.4}/test/test_pull.py +0 -0
  42. {git_nested-1.0.3 → git_nested-1.0.4}/test/test_push.py +0 -0
  43. {git_nested-1.0.3 → git_nested-1.0.4}/test/test_rebase.py +0 -0
  44. {git_nested-1.0.3 → git_nested-1.0.4}/test/test_reclone.py +0 -0
  45. {git_nested-1.0.3 → git_nested-1.0.4}/test/test_status.py +0 -0
  46. {git_nested-1.0.3 → git_nested-1.0.4}/test/test_submodule.py +0 -0
  47. {git_nested-1.0.3 → git_nested-1.0.4}/test/test_version.py +0 -0
  48. {git_nested-1.0.3 → git_nested-1.0.4}/test/unittest/test_logging.py +0 -0
  49. {git_nested-1.0.3 → git_nested-1.0.4}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: git-nested
3
- Version: 1.0.3
3
+ Version: 1.0.4
4
4
  Summary: Git Submodule Alternative
5
5
  Author-email: Thorsten Klein <thorsten.klein.git@gmail.com>
6
6
  License: MIT
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: git-nested
3
- Version: 1.0.3
3
+ Version: 1.0.4
4
4
  Summary: Git Submodule Alternative
5
5
  Author-email: Thorsten Klein <thorsten.klein.git@gmail.com>
6
6
  License: MIT
@@ -12,6 +12,7 @@ import subprocess
12
12
  import argparse
13
13
  import re
14
14
  import shutil
15
+ import tempfile
15
16
  import textwrap
16
17
  import yaml
17
18
  from importlib.metadata import version as _pkg_version, PackageNotFoundError
@@ -474,12 +475,22 @@ class GitNestedRepo:
474
475
  method = flags.method or config.method
475
476
  refs_fetch = f'refs/nested/{subref}/fetch'
476
477
 
478
+ merge_target = refs_fetch
479
+ if config.filter:
480
+ # The local nested branch only ever contained the filtered subset of
481
+ # files. Merging the raw (unfiltered) upstream fetch against it makes
482
+ # git see files that were "deleted" locally (because they were never
483
+ # pulled in) as delete/modify conflicts whenever upstream touches
484
+ # them. Build a filtered view of the fetched commit so only files
485
+ # that actually matter to the local checkout can conflict.
486
+ merge_target = self.build_filtered_commit(git, subdir_worktree, config, upstream_head_commit)
487
+
477
488
  try:
478
489
  if method == 'rebase':
479
- git.run(['rebase', refs_fetch, branch], cwd=subdir_worktree, print_error=False)
490
+ git.run(['rebase', merge_target, branch], cwd=subdir_worktree, print_error=False)
480
491
  else:
481
492
  # If parent is empty, allow unrelated histories (for nested-in-nested repos)
482
- merge_cmd = ['merge', refs_fetch]
493
+ merge_cmd = ['merge', merge_target]
483
494
  if not config.parent:
484
495
  merge_cmd.append('--allow-unrelated-histories')
485
496
  git.run(merge_cmd, cwd=subdir_worktree, print_error=False)
@@ -492,6 +503,61 @@ class GitNestedRepo:
492
503
 
493
504
  return True, nested_commit_ref, subdir_worktree, None
494
505
 
506
+ def build_filtered_commit(self, git: GitRunner, cwd: Path, config: NestedConfig, commit: str) -> str:
507
+ """Build a throwaway commit whose tree only contains the paths matched by
508
+ config.filter, applying the exact same literal/regex rules used when the
509
+ filtered content is written into the working tree.
510
+
511
+ The new commit keeps `commit` as its sole parent so its ancestry (and
512
+ thus merge-base detection against the local nested branch) is unaffected.
513
+
514
+ Returns:
515
+ sha of the filtered commit
516
+ """
517
+ with tempfile.TemporaryDirectory() as tmp:
518
+ index_file = Path(tmp) / 'index'
519
+ env = os.environ.copy()
520
+ env['GIT_INDEX_FILE'] = str(index_file)
521
+
522
+ git.run(['read-tree', '--empty'], cwd=cwd, env=env)
523
+
524
+ def already_indexed(path: str) -> bool:
525
+ return bool(git.check_output(['ls-files', '--stage', '--', path], cwd=cwd, env=env, may_fail=True))
526
+
527
+ regex_patterns = []
528
+ for p in config.filter:
529
+ obj_type = git.check_output(['cat-file', '-t', f'{commit}:{p}'], may_fail=True, cwd=cwd)
530
+ if obj_type == 'tree':
531
+ git.run(['read-tree', f'--prefix={p}', f'{commit}:{p}'], cwd=cwd, env=env)
532
+ elif obj_type == 'blob':
533
+ mode = git.check_output(['ls-tree', commit, '--', p]).split()[0]
534
+ blob_sha = git.check_output(['rev-parse', f'{commit}:{p}'], cwd=cwd)
535
+ git.run(['update-index', '--add', '--cacheinfo', f'{mode},{blob_sha},{p}'], cwd=cwd, env=env)
536
+ else:
537
+ try:
538
+ regex_patterns.append(re.compile(p))
539
+ except re.error as e:
540
+ raise GitNestedError(f"Invalid filter pattern '{p}': {e}")
541
+
542
+ if regex_patterns:
543
+ tree_listing = git.check_output(['ls-tree', '-r', commit], may_fail=True, cwd=cwd) or ''
544
+ for line in tree_listing.splitlines():
545
+ meta, blob_path = line.split('\t', 1)
546
+ if not any(pattern.fullmatch(blob_path) for pattern in regex_patterns):
547
+ continue
548
+ if already_indexed(blob_path):
549
+ continue
550
+ mode, _obj_type, blob_sha = meta.split()
551
+ git.run(
552
+ ['update-index', '--add', '--cacheinfo', f'{mode},{blob_sha},{blob_path}'], cwd=cwd, env=env
553
+ )
554
+
555
+ filtered_tree = git.check_output(['write-tree'], cwd=cwd, env=env)
556
+ filtered_commit = git.check_output(
557
+ ['commit-tree', '-p', commit, '-m', 'git-nested: filtered view for merge', filtered_tree], cwd=cwd
558
+ )
559
+ return filtered_commit
560
+
495
561
  def do_push(
496
562
  self,
497
563
  git: GitRunner,
@@ -52,6 +52,39 @@ def test_consumer_with_filter(foo_bar_cloned):
52
52
  assert (env.workspace / 'foo' / 'leg' / 'subdirC').exists()
53
53
 
54
54
 
55
+ def test_pull_conflicts_on_change_to_filtered_out_file(foo_bar_cloned):
56
+ """Reproduces bug: pulling upstream changes to a file excluded by --filter
57
+ causes a merge conflict, even though the file was never present locally.
58
+ """
59
+ env = foo_bar_cloned
60
+
61
+ create_upstream_repo(env.upstream / 'leg')
62
+ clone_repo(str(env.upstream / 'leg'), env.workspace / 'leg')
63
+
64
+ env.add_new_files('subdirA/somefile', cwd=env.workspace / 'leg')
65
+ env.add_new_files('subdirB/somefile', cwd=env.workspace / 'leg')
66
+ env.run(['git', 'push'], cwd=env.workspace / 'leg')
67
+
68
+ # clone leg with a filter that excludes subdirB entirely
69
+ cmd_git_nested(f'clone {env.upstream}/leg leg --filter=subdirA', cwd=env.workspace / 'foo')
70
+ assert (env.workspace / 'foo' / 'leg' / 'subdirA').exists()
71
+ assert not (env.workspace / 'foo' / 'leg' / 'subdirB').exists()
72
+
73
+ # upstream modifies a file inside the *excluded* subdirB - the local clone
74
+ # never had this file, so this change should not conflict with anything
75
+ env.modify_files('subdirB/somefile', cwd=env.workspace / 'leg')
76
+ env.run(['git', 'push'], cwd=env.workspace / 'leg')
77
+
78
+ # pulling should succeed cleanly since subdirB is filtered out locally
79
+ result = cmd_git_nested('pull leg', cwd=env.workspace / 'foo', check=False)
80
+ assert result.returncode == 0, (
81
+ f"Expected pull of a change to a filtered-out file to succeed without conflicts, "
82
+ f"but it failed:\n{result.stdout}\n{result.stderr}"
83
+ )
84
+ assert (env.workspace / 'foo' / 'leg' / 'subdirA').exists()
85
+ assert not (env.workspace / 'foo' / 'leg' / 'subdirB').exists()
86
+
87
+
55
88
  def test_filter_pull(foo_bar_cloned):
56
89
  """Test that a filter added to .gitnested is applied on the next pull"""
57
90
  env = foo_bar_cloned
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes