Repository: pdm-project/pdm

Issue:
`pdm export --self` is incompatible with hashes (no `--without-hashes`)
- [x] I have searched the issue tracker and believe that this is not a duplicate.

## Steps to reproduce
```sh
pdm add requests
pdm export --self -o requirements.txt
```

The command creates this file

```
# This file is @generated by PDM.
# Please do not edit it manually.

certifi==2024.2.2 \
    --hash=sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f \
    --hash=sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1
charset-normalizer==3.3.2 \
    --hash=sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8 \
    --hash=sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc \
    --hash=sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6 \
    --hash=sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed \
    --hash=sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068 \
    --hash=sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26 \
    --hash=sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d \
    --hash=sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa \
    --hash=sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a \
    --hash=sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b \
    --hash=sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001 \
    --hash=sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389 \
    --hash=sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143 \
    --hash=sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7 \
    --hash=sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b \
    --hash=sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4 \
    --hash=sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5
idna==3.7 \
    --hash=sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc \
    --hash=sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0
requests==2.32.2 \
    --hash=sha256:dd951ff5ecf3e3b3aa26b40703ba77495dab41da839ae72ef3c8e5d8e2433289 \
    --hash=sha256:fc06670dd0ed212426dfeb94fc1b983d917c4f9847c863f313c9dfaaffb7c23c
urllib3==2.2.1 \
    --hash=sha256:450b20ec296a467077128bff42b73080516e71b56ff59a60a02bef2232c4fa9d \
    --hash=sha256:d0570876c61ab9e520d776c38acbbb5b05a776d3f9ff98a5c8fd5162a444cf19
.  # this package
```

## Actual behavior

Feeding this file into `pip install -r` will create an error, likely due to pypa/pip#4995

```
Processing /project
ERROR: Can't verify hashes for these file:// requirements because they point to directories:
    file:///project (from -r requirements.txt (line 34))
```

## Expected behavior

<!--A clear and concise description of what you expected to happen.-->
I believe a warning should be displayed when `--self` is used without `--no-hashes`, as it will create a file which does not work with pip. The same should be done for any VCS or file:// dependencies.


Discussion:

Patch:
diff --git a/news/2908.bugfix.md b/news/2908.bugfix.md
new file mode 100644
index 0000000000..5f1da2d6f4
--- /dev/null
+++ b/news/2908.bugfix.md
@@ -0,0 +1,1 @@
+Fix a bug that VCS dependencies and `--self` don't work in the exported requirements.txt with hashes.
diff --git a/src/pdm/cli/commands/add.py b/src/pdm/cli/commands/add.py
index df4d2ce1e2..76db572840 100644
--- a/src/pdm/cli/commands/add.py
+++ b/src/pdm/cli/commands/add.py
@@ -155,8 +155,9 @@ def do_add(
         reqs = [
             r for g, deps in all_dependencies.items() if lock_groups is None or g in lock_groups for r in deps.values()
         ]
-        # pre-write the dependencies to the pyproject.toml to make them recognized by the resolver
-        project.add_dependencies(requirements, group, selection.dev or False, write=False)
+        # pre-write the named dependencies to the pyproject.toml to make them recognized by the resolver
+        named_req = {k: r for k, r in requirements.items() if r.is_named}
+        project.add_dependencies(named_req, group, selection.dev or False, write=False)
         with hooks.skipping("post_lock"):
             resolved = do_lock(
                 project,
diff --git a/src/pdm/formats/requirements.py b/src/pdm/formats/requirements.py
index 0e40c279c2..f4ff02b9bb 100644
--- a/src/pdm/formats/requirements.py
+++ b/src/pdm/formats/requirements.py
@@ -189,6 +189,8 @@ def export(
     for candidate in sorted(candidates, key=lambda x: x.identify()):  # type: ignore[attr-defined]
         if isinstance(candidate, Candidate):
             req = candidate.req.as_pinned_version(candidate.version)
+            if options.hashes and not candidate.hashes:
+                raise PdmUsageError(f"Hash is not available for '{req}', please export with `--no-hashes` option.")
         else:
             assert isinstance(candidate, Requirement)
             req = candidate
@@ -203,6 +205,8 @@ def export(
         lines.append("\n")
     if (options.self or options.editable_self) and not project.is_distribution:
         raise PdmUsageError("Cannot export the project itself in a non-library project.")
+    if options.hashes and (options.self or options.editable_self):
+        raise PdmUsageError("Hash is not available for `--self/--editable-self`. Please export with `--no-hashes`.")
     if options.self:
         lines.append(".  # this package\n")
     elif options.editable_self:


Test patch:
diff --git a/tests/cli/test_others.py b/tests/cli/test_others.py
index 90541ce0ec..7a5123dc21 100644
--- a/tests/cli/test_others.py
+++ b/tests/cli/test_others.py
@@ -176,10 +176,16 @@ def test_export_to_requirements_txt(pdm, fixture_project):
     assert result.output.strip() == requirements_txt.read_text().strip()
 
     result = pdm(["export", "--self"], obj=project)
+    assert result.exit_code == 1
+
+    result = pdm(["export", "--editable-self"], obj=project)
+    assert result.exit_code == 1
+
+    result = pdm(["export", "--no-hashes", "--self"], obj=project)
     assert result.exit_code == 0
     assert ".  # this package\n" in result.output.strip()
 
-    result = pdm(["export", "--editable-self"], obj=project)
+    result = pdm(["export", "--no-hashes", "--editable-self"], obj=project)
     assert result.exit_code == 0
     assert "-e .  # this package\n" in result.output.strip()
 
@@ -227,8 +233,19 @@ def test_show_update_hint(pdm, project, monkeypatch):
 @pytest.mark.usefixtures("repository")
 def test_export_with_platform_markers(pdm, project):
     pdm(["add", "--no-sync", 'urllib3; sys_platform == "fake"'], obj=project, strict=True)
-    result = pdm(["export"], obj=project, strict=True)
+    result = pdm(["export", "--no-hashes"], obj=project, strict=True)
     assert 'urllib3==1.22; sys_platform == "fake"' in result.output.splitlines()
 
-    result = pdm(["export", "--no-markers"], obj=project, strict=True)
+    result = pdm(["export", "--no-markers", "--no-hashes"], obj=project, strict=True)
     assert not any("urllib3" in line for line in result.output.splitlines())
+
+
+@pytest.mark.usefixtures("repository", "vcs")
+def test_export_with_vcs_deps(pdm, project):
+    pdm(["add", "--no-sync", "git+https://github.com/test-root/demo.git"], obj=project, strict=True)
+    result = pdm(["export"], obj=project)
+    assert result.exit_code != 0
+
+    result = pdm(["export", "--no-hashes"], obj=project)
+    assert result.exit_code == 0
+    assert "demo @ git+https://github.com/test-root/demo.git@1234567890abcdef" in result.output.splitlines()
diff --git a/tests/test_formats.py b/tests/test_formats.py
index 5283111823..874b2ab924 100644
--- a/tests/test_formats.py
+++ b/tests/test_formats.py
@@ -175,12 +175,12 @@ def test_import_requirements_with_group(project):
 
 
 def test_export_requirements_with_self(project):
-    result = requirements.export(project, [], ns(self=True))
+    result = requirements.export(project, [], ns(self=True, hashes=False))
     assert result.strip().splitlines()[-1] == ".  # this package"
 
 
 def test_export_requirements_with_editable_self(project):
-    result = requirements.export(project, [], ns(editable_self=True))
+    result = requirements.export(project, [], ns(editable_self=True, hashes=False))
     assert result.strip().splitlines()[-1] == "-e .  # this package"