Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ True
('there was a fix\n', 2)
```

## Git file modes, symlinks and line numbers
## Git file modes, symlinks, submodules and line numbers

For git diffs, the file mode is exposed through the `source_mode` and
`target_mode` attributes (e.g. `'100644'`, `'100755'`, `'120000'`), or `None`
Expand All @@ -178,6 +178,21 @@ True
True
```

Likewise, `is_submodule` detects git submodule (gitlink) entries, mode
`160000`:

```python
>>> from unidiff import PatchSet
>>> patch = PatchSet.from_filename('tests/samples/git_submodule.diff')
>>> patched_file = patch[0]
>>> patched_file.path
'tests/diffs/submodule'
>>> patched_file.target_mode
'160000'
>>> patched_file.is_submodule
True
```

Each `PatchedFile` also exposes `diff_line_no`, the 1-based line number in the
diff where its entry starts. This is useful to locate files that have no hunks,
such as binary changes:
Expand Down
7 changes: 7 additions & 0 deletions tests/samples/git_submodule.diff
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
diff --git a/tests/diffs/submodule b/tests/diffs/submodule
new file mode 160000
index 0000000..b399108
--- /dev/null
+++ b/tests/diffs/submodule
@@ -0,0 +1 @@
+Subproject commit b399108e316b17e4e6eed616d112c33796289533
51 changes: 51 additions & 0 deletions tests/test_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -615,6 +615,57 @@ def test_added_symlink_file_mode(self):
self.assertEqual(res[0].target_mode, '120000')
self.assertTrue(res[0].is_symlink)

def test_added_submodule_file_mode(self):
# issue #147: a new git submodule (gitlink) has mode 160000
filename = os.path.join(self.samples_dir, 'samples/git_submodule.diff')
with open(filename) as f:
res = PatchSet(f)

self.assertEqual(len(res), 1)
self.assertTrue(res[0].is_added_file)
self.assertIsNone(res[0].source_mode)
self.assertEqual(res[0].target_mode, '160000')
self.assertTrue(res[0].is_submodule)
self.assertFalse(res[0].is_symlink)

def test_deleted_submodule_file_mode(self):
# issue #147: a removed submodule only carries the old mode
diff = (
'diff --git a/submodule b/submodule\n'
'deleted file mode 160000\n'
'index b399108..0000000\n'
'--- a/submodule\n'
'+++ /dev/null\n'
'@@ -1 +0,0 @@\n'
'-Subproject commit b399108e316b17e4e6eed616d112c33796289533\n'
)
res = PatchSet(diff)

self.assertTrue(res[0].is_removed_file)
self.assertEqual(res[0].source_mode, '160000')
self.assertIsNone(res[0].target_mode)
self.assertTrue(res[0].is_submodule)

def test_updated_submodule_file_mode(self):
# issue #147: a submodule pointer update carries the mode on the
# index line
diff = (
'diff --git a/submodule b/submodule\n'
'index b399108..c0ffee1 160000\n'
'--- a/submodule\n'
'+++ b/submodule\n'
'@@ -1 +1 @@\n'
'-Subproject commit b399108e316b17e4e6eed616d112c33796289533\n'
'+Subproject commit c0ffee1e316b17e4e6eed616d112c33796289533\n'
)
res = PatchSet(diff)

self.assertEqual(res[0].source_mode, '160000')
self.assertEqual(res[0].target_mode, '160000')
self.assertTrue(res[0].is_submodule)
self.assertFalse(res[0].is_symlink)
self.assertTrue(res[0].is_modified_file)

def test_new_file_mode(self):
# issue #125: a regular new file carries `new file mode 100644`
filename = os.path.join(self.samples_dir, 'samples/git_quoted_filename.diff')
Expand Down
3 changes: 3 additions & 0 deletions unidiff/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,9 @@
# git file mode for a symbolic link
SYMLINK_FILE_MODE = '120000'

# git file mode for a submodule (gitlink) entry
SUBMODULE_FILE_MODE = '160000'

LINE_TYPE_ADDED = '+'
LINE_TYPE_REMOVED = '-'
LINE_TYPE_CONTEXT = ' '
Expand Down
18 changes: 14 additions & 4 deletions unidiff/patch.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
RE_NO_NEWLINE_MARKER,
RE_BINARY_DIFF,
RE_PATCH_FILE_PREFIX,
SUBMODULE_FILE_MODE,
SYMLINK_FILE_MODE,
)
from unidiff.errors import UnidiffParseError
Expand Down Expand Up @@ -421,12 +422,21 @@ def is_modified_file(self) -> bool:
return not (self.is_added_file or self.is_removed_file)

@property
def is_symlink(self) -> bool:
"""Return True if the patched file is a symbolic link."""
def _file_mode(self) -> Optional[str]:
"""Return the relevant git file mode, if known."""
# prefer the target mode; fall back to the source mode (e.g. a
# removed symlink only carries the old mode)
mode = self.target_mode if self.target_mode is not None else self.source_mode
return mode == SYMLINK_FILE_MODE
return self.target_mode if self.target_mode is not None else self.source_mode

@property
def is_symlink(self) -> bool:
"""Return True if the patched file is a symbolic link."""
return self._file_mode == SYMLINK_FILE_MODE

@property
def is_submodule(self) -> bool:
"""Return True if the patched file is a git submodule (gitlink)."""
return self._file_mode == SUBMODULE_FILE_MODE


class PatchSet(list[PatchedFile]):
Expand Down
Loading