nebius/SWE-rebench-V2-PRs huggingface dataset:
Repository: keon/algorithms
Pull request #281 (2018-05-16 08:08:01)

Add is_rotated.py
I added an algorithm to strings. This checks if a string is a rotated version of another string and returns a bool. A few tests were added to test_strings.py as well.

coveralls: ## Pull Request Test Coverage Report for [Build 257](https://coveralls.io/builds/17007241)

* **11** of **11**   **(100.0%)**  changed or added relevant lines in **2** files are covered.
* No unchanged relevant lines lost coverage.
* Overall coverage increased (+**0.03%**) to **91.25%**

---



|  Totals | [![Coverage Status](https://coveralls.io/builds/17007241/badge)](https://coveralls.io/builds/17007241) |
| :-- | --: |
| Change from base [Build 256](https://coveralls.io/builds/16947354): |  0.03% |
| Covered Lines: | 2753 |
| Relevant Lines: | 3017 |

---
##### 💛  - [Coveralls](https://coveralls.io)

danghai: @victore07 Thanks your PR. I am going to merge when you fix a minor
danghai: Thanks

Patch:
diff --git a/strings/is_rotated.py b/strings/is_rotated.py
new file mode 100644
index 0000000..a68d62d
--- /dev/null
+++ b/strings/is_rotated.py
@@ -0,0 +1,15 @@
+"""
+Given two strings s1 and s2, determine if s2 is a rotated version of s1.
+For example,
+is_rotated("hello", "llohe") returns True
+is_rotated("hello", "helol") returns False
+
+accepts two strings
+returns bool
+"""
+
+def is_rotated(s1, s2):
+    if len(s1) == len(s2):
+        return s2 in s1 + s1
+    else:
+        return False
\ No newline at end of file


Test patch:
diff --git a/tests/test_strings.py b/tests/test_strings.py
index 8cee75b..c0aa283 100644
--- a/tests/test_strings.py
+++ b/tests/test_strings.py
@@ -8,6 +8,7 @@ from strings.group_anagrams import group_anagrams
 from strings.int_to_roman import int_to_roman
 from strings.is_palindrome import is_palindrome, is_palindrome_reverse, \
 is_palindrome_two_pointer, is_palindrome_stack
+from strings.is_rotated import is_rotated
 from strings.license_number import license_number
 from strings.make_sentence import make_sentence
 from strings.merge_string_checker import is_merge_recursive, is_merge_iterative
@@ -165,6 +166,22 @@ class TestIsPalindrome(unittest.TestCase):
         self.assertFalse(is_palindrome_stack("house"))
 
 
+class TestIsRotated(unittest.TestCase):
+    """[summary]
+    Test for the file is_rotated.py
+
+    Arguments:
+        unittest {[type]} -- [description]
+    """
+
+    def test_is_rotated(self):
+        self.assertTrue(is_rotated("hello", "hello"))
+        self.assertTrue(is_rotated("hello", "llohe"))
+        self.assertFalse(is_rotated("hello", "helol"))
+        self.assertFalse(is_rotated("hello", "lloh"))
+        self.assertTrue(is_rotated("", ""))
+
+
 class TestLicenseNumber(unittest.TestCase):
     """[summary]
     Test for the file license_number.py
Repository: keon/algorithms
Pull request #358 (2018-06-17 05:21:31)

improvement combination.py using memoization
#357 I try to improve combination.py


(Put an `X` inside the `[ ]` to denote check mark `[X]`.)


- [ ] **If creating a new file :**
  - [ ] added links to it in the README files ?
  - [ ] included tests with it ?
  - [ ] added description (overview of algorithm, time and space compleixty, and possible edge case) in docstrings ?

- [X] **if done some changes :**
  - [X] wrote short description in the PR explaining what the changes do ?
  - [X] Fixes #[issue number] if related to any issue

- [ ] **other**


arsgsg1: I forgot check parameter 
combination_memo(50, 10) <- before
combination_memo(50, 10) <- after
coveralls: ## Pull Request Test Coverage Report for [Build 640](https://coveralls.io/builds/17534576)

* **10** of **10**   **(100.0%)**  changed or added relevant lines in **1** file are covered.
* No unchanged relevant lines lost coverage.
* Overall coverage increased (+**0.1%**) to **70.951%**

---



|  Totals | [![Coverage Status](https://coveralls.io/builds/17534576/badge)](https://coveralls.io/builds/17534576) |
| :-- | --: |
| Change from base [Build 638](https://coveralls.io/builds/17528331): |  0.1% |
| Covered Lines: | 3632 |
| Relevant Lines: | 5119 |

---
##### 💛  - [Coveralls](https://coveralls.io)

huseyindalbudak: you can use factorial and combination function.
https://github.com/huseyindalbudak/mathpy/blob/master/combination.py
the code is so faster for combination(50, 10) than keon algorithms

Patch:
diff --git a/algorithms/maths/combination.py b/algorithms/maths/combination.py
index ea9e4b3..308b0bc 100644
--- a/algorithms/maths/combination.py
+++ b/algorithms/maths/combination.py
@@ -1,6 +1,17 @@
 def combination(n, r):
-    # This function calculates nCr
+    """This function calculates nCr."""
     if n == r or r == 0:
         return 1
     else:
         return combination(n-1, r-1) + combination(n-1, r)
+
+def combination_memo(n, r):
+    """This function calculates nCr using memoization method."""
+    memo = {}
+    def recur(n, r):
+        if n == r or r == 0:
+            return 1
+        if (n, r) not in memo:
+            memo[(n, r)] = recur(n - 1, r - 1) + recur(n - 1, r)
+        return memo[(n, r)]
+    return recur(n, r)


Test patch:
diff --git a/tests/test_maths.py b/tests/test_maths.py
index 1514907..9270c28 100644
--- a/tests/test_maths.py
+++ b/tests/test_maths.py
@@ -12,7 +12,7 @@ from algorithms.maths import (
     pythagoras,
     is_prime,
     encrypt, decrypt, generate_key,
-    combination
+    combination, combination_memo
 )
 
 import unittest
@@ -230,7 +230,9 @@ class TestCombination(unittest.TestCase):
     def test_combination(self):
         self.assertEqual(10, combination(5, 2))
         self.assertEqual(252, combination(10, 5))
-        
+    def test_combination_memo(self):
+        self.assertEqual(10272278170, combination_memo(50, 10))
+        self.assertEqual(847660528, combination_memo(40, 10))
 class TestFactorial(unittest.TestCase):
     """[summary]
     Test for the file factorial.py
Repository: keon/algorithms
Pull request #434 (2018-10-24 01:09:54)

Add hailstone algorithm
(Put an `X` inside the `[ ]` to denote check mark `[X]`.)


- [X] **If creating a new file :**
  - [X] added links to it in the README files ?
  - [X] included tests with it ?
  - [X] added description (overview of algorithm, time and space compleixty, and possible edge case) in docstrings ?

- [ ] **if done some changes :**
  - [ ] wrote short description in the PR explaining what the changes do ?
  - [ ] Fixes #[issue number] if related to any issue

- [ ] **other**


coveralls: ## Pull Request Test Coverage Report for [Build 794](https://coveralls.io/builds/19684654)

* **12** of **12**   **(100.0%)**  changed or added relevant lines in **2** files are covered.
* No unchanged relevant lines lost coverage.
* Overall coverage increased (+**0.06%**) to **67.923%**

---



|  Totals | [![Coverage Status](https://coveralls.io/builds/19684654/badge)](https://coveralls.io/builds/19684654) |
| :-- | --: |
| Change from base [Build 789](https://coveralls.io/builds/19626595): |  0.06% |
| Covered Lines: | 4415 |
| Relevant Lines: | 6500 |

---
##### 💛  - [Coveralls](https://coveralls.io)

keon: thanks!

Patch:
diff --git a/README.md b/README.md
index aecd53b..4443884 100644
--- a/README.md
+++ b/README.md
@@ -203,6 +203,7 @@ If you want to uninstall algorithms, it is as simple as:
     - [rsa](algorithms/maths/rsa.py)
     - [sqrt_precision_factor](algorithms/maths/sqrt_precision_factor.py)
     - [summing_digits](algorithms/maths/summing_digits.py)
+    - [hailstone](algorithms/maths/hailstone.py)
 - [matrix](algorithms/matrix)
     - [sudoku_validator](algorithms/matrix/sudoku_validator.py)
     - [bomb_enemy](algorithms/matrix/bomb_enemy.py)
diff --git a/algorithms/maths/hailstone.py b/algorithms/maths/hailstone.py
new file mode 100644
index 0000000..2232123
--- /dev/null
+++ b/algorithms/maths/hailstone.py
@@ -0,0 +1,13 @@
+def hailstone(n):
+  """Return the 'hailstone sequence' from n to 1
+     n: The starting point of the hailstone sequence
+  """
+
+  sequence = [n]
+  while n > 1:
+    if n%2 != 0:
+      n = 3*n + 1
+    else: 
+      n = int(n/2)
+    sequence.append(n)
+  return sequence
\ No newline at end of file


Test patch:
diff --git a/tests/test_maths.py b/tests/test_maths.py
index 0777f9a..dd1d0e1 100644
--- a/tests/test_maths.py
+++ b/tests/test_maths.py
@@ -14,7 +14,8 @@ from algorithms.maths import (
     pythagoras,
     is_prime,
     encrypt, decrypt,
-    combination, combination_memo
+    combination, combination_memo,
+    hailstone,
 )
 
 import unittest
@@ -288,5 +289,17 @@ class TestFactorial(unittest.TestCase):
         self.assertRaises(ValueError, factorial_recur, 42, -1)
 
 
+class TestHailstone(unittest.TestCase):
+    """[summary]
+    Test for the file hailstone.py
+
+    Arguments:
+        unittest {[type]} -- [description]
+    """
+    def test_hailstone(self):
+        self.assertEqual([8, 4, 2, 1], hailstone.hailstone(8))
+        self.assertEqual([10, 5, 16, 8, 4, 2, 1], hailstone.hailstone(10))
+
+
 if __name__ == "__main__":
     unittest.main()
Repository: keon/algorithms
Pull request #572 (2019-10-22 19:13:43)

Add histogram algorithm
(Put an `X` inside the `[ ]` to denote check mark `[X]`.)

Add histogram algorithm, referenced in issue #569

- [X] **If creating a new file :**
  - [X] added links to it in the README files ?
  - [X] included tests with it ?
  - [X] added description (overview of algorithm, time and space complexity, and possible edge case) in docstrings ?

- [ ] **if done some changes :**
  - [ ] wrote short description in the PR explaining what the changes do ?
  - [ ] Fixes #[issue number] if related to any issue

- [ ] **other**



Patch:
diff --git a/README.md b/README.md
index 2e79f5d..a127f59 100644
--- a/README.md
+++ b/README.md
@@ -123,6 +123,8 @@ If you want to uninstall algorithms, it is as simple as:
     - [pacific_atlantic](algorithms/dfs/pacific_atlantic.py)
     - [sudoku_solver](algorithms/dfs/sudoku_solver.py)
     - [walls_and_gates](algorithms/dfs/walls_and_gates.py)
+- [distribution](algorithms/distribution)
+    - [histogram](algorithms/distribution/histogram.py)
 - [dp](algorithms/dp)
     - [buy_sell_stock](algorithms/dp/buy_sell_stock.py)
     - [climbing_stairs](algorithms/dp/climbing_stairs.py)
diff --git a/algorithms/distribution/__init__.py b/algorithms/distribution/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/algorithms/distribution/histogram.py b/algorithms/distribution/histogram.py
new file mode 100644
index 0000000..7bcdc7f
--- /dev/null
+++ b/algorithms/distribution/histogram.py
@@ -0,0 +1,28 @@
+"""
+Histogram function.
+
+Histogram is an accurate representation of the distribution of numerical data.
+It is an estimate of the probability distribution of a continuous variable.
+https://en.wikipedia.org/wiki/Histogram
+
+Example:
+    list_1 = [3, 3, 2, 1]
+    :return {1: 1, 2: 1, 3: 2}
+
+    list_2 = [2, 3, 5, 5, 5, 6, 4, 3, 7]
+    :return {2: 1, 3: 2, 4: 1, 5: 3, 6: 1, 7: 1}
+"""
+
+
+def get_histogram(input_list: list) -> dict:
+    """
+    Get histogram representation
+    :param input_list: list with different and unordered values
+    :return histogram: dict with histogram of input_list
+    """
+    # Create dict to store histogram
+    histogram = {}
+    # For each list value, add one to the respective histogram dict position
+    for i in input_list:
+        histogram[i] = histogram.get(i, 0) + 1
+    return histogram


Test patch:
diff --git a/tests/test_histogram.py b/tests/test_histogram.py
new file mode 100644
index 0000000..cd35bd2
--- /dev/null
+++ b/tests/test_histogram.py
@@ -0,0 +1,17 @@
+from algorithms.distribution.histogram import get_histogram
+
+import unittest
+
+
+class TestListsInHistogram(unittest.TestCase):
+    def test_histogram(self):
+        list_1 = [3, 3, 2, 1]
+        list_2 = [2, 3, 5, 5, 5, 6, 4, 3, 7]
+
+        self.assertEqual(get_histogram(list_1), {1: 1, 2: 1, 3: 2})
+        self.assertEqual(get_histogram(list_2),
+                         {2: 1, 3: 2, 4: 1, 5: 3, 6: 1, 7: 1})
+
+
+if __name__ == '__main__':
+    unittest.main()
Repository: keon/algorithms
Pull request #609 (2019-12-06 05:20:27)

Add gcd bit
(Put an `X` inside the `[ ]` to denote check mark `[X]`.)


- [ ] **If creating a new file :**
  - [ ] added links to it in the README files ?
  - [ ] included tests with it ?
  - [ ] added description (overview of algorithm, time and space complexity, and possible edge case) in docstrings ?

- [X] **if done some changes :**
  - [X] wrote short description in the PR explaining what the changes do ?
  - [ ] Fixes #[issue number] if related to any issue

- [ ] **other**



Patch:
diff --git a/algorithms/maths/gcd.py b/algorithms/maths/gcd.py
index 1f80fc9..002f4fc 100644
--- a/algorithms/maths/gcd.py
+++ b/algorithms/maths/gcd.py
@@ -10,3 +10,42 @@ def gcd(a, b):
 def lcm(a, b):
     """Computes the lowest common multiple of integers a and b."""
     return a * b / gcd(a, b)
+
+
+"""
+Given a positive integer x, computes the number of trailing zero of x.
+Example
+Input : 34(100010)
+           ~~~~~^
+Output : 1
+
+Input : 40(101000)
+           ~~~^^^
+Output : 3
+"""
+
+def trailing_zero(x):
+    cnt = 0
+    while x and not x & 1:
+        cnt += 1
+        x >>= 1
+    return cnt
+
+"""
+Given two non-negative integer a and b, 
+computes the greatest common divisor of a and b using bitwise operator.
+"""
+
+def gcd_bit(a, b):
+    tza = trailing_zero(a)
+    tzb = trailing_zero(b)
+    a >>= tza
+    b >>= tzb
+    while b:
+        if a < b: 
+            a, b = b, a
+        a -= b
+        a >>= trailing_zero(a)
+    return a << min(tza, tzb)
+
+


Test patch:
diff --git a/tests/test_maths.py b/tests/test_maths.py
index 850cd5d..86b61e0 100644
--- a/tests/test_maths.py
+++ b/tests/test_maths.py
@@ -4,7 +4,7 @@ from algorithms.maths import (
     euler_totient,
     extended_gcd,
     factorial, factorial_recur,
-    gcd, lcm,
+    gcd, lcm, trailing_zero, gcd_bit,
     gen_strobogrammatic, strobogrammatic_in_range,
     is_strobogrammatic, is_strobogrammatic2,
     modular_exponential,
@@ -101,6 +101,13 @@ class TestGcd(unittest.TestCase):
     def test_lcm(self):
         self.assertEqual(24, lcm(8, 12))
 
+    def test_trailing_zero(self):
+        self.assertEqual(1, trailing_zero(34))
+        self.assertEqual(3, trailing_zero(40))
+
+    def test_gcd_bit(self):
+        self.assertEqual(4, gcd_bit(8, 12))
+        self.assertEqual(1, gcd(13, 17))
 
 class TestGenerateStroboGrammatic(unittest.TestCase):
     """[summary]
@@ -320,3 +327,4 @@ class TestCosineSimilarity(unittest.TestCase):
 
 if __name__ == "__main__":
     unittest.main()
+
Repository: keon/algorithms
Pull request #668 (2020-06-29 21:04:46)

add pigeonhole sort
Add pigeonhole sort to the sort folder + add test for it


Patch:
diff --git a/README.md b/README.md
index bb6c4c0..e26c02d 100644
--- a/README.md
+++ b/README.md
@@ -279,6 +279,7 @@ If you want to uninstall algorithms, it is as simple as:
     - [meeting_rooms](algorithms/sort/meeting_rooms.py)
     - [merge_sort](algorithms/sort/merge_sort.py)
     - [pancake_sort](algorithms/sort/pancake_sort.py)
+    - [pigeonhole_sort](algorithms/sort/pigeonhole_sort.py)
     - [quick_sort](algorithms/sort/quick_sort.py)
     - [radix_sort](algorithms/sort/radix_sort.py)
     - [selection_sort](algorithms/sort/selection_sort.py)
diff --git a/algorithms/sort/__init__.py b/algorithms/sort/__init__.py
index 5a870fe..b948dba 100644
--- a/algorithms/sort/__init__.py
+++ b/algorithms/sort/__init__.py
@@ -8,6 +8,7 @@ from .heap_sort import *
 from .insertion_sort import *
 from .merge_sort import *
 from .pancake_sort import *
+from .pigeonhole_sort import *
 from .quick_sort import *
 from .selection_sort import *
 from .top_sort import *
diff --git a/algorithms/sort/pigeonhole_sort.py b/algorithms/sort/pigeonhole_sort.py
new file mode 100644
index 0000000..0569aa9
--- /dev/null
+++ b/algorithms/sort/pigeonhole_sort.py
@@ -0,0 +1,28 @@
+"""
+
+https://en.wikipedia.org/wiki/Pigeonhole_sort
+
+Time complexity: O(n + Range) where n = number of elements and Range = possible values in the array
+
+Suitable for lists where the number of elements and key values are mostly the same.
+
+"""
+
+
+def pigeonhole_sort(arr):
+    Max = max(arr)
+    Min = min(arr)
+    size = Max - Min + 1
+
+    holes = [0]*size
+
+    for i in arr:
+        holes[i-Min] += 1
+
+    i = 0
+    for count in range(size):
+        while holes[count] > 0:
+            holes[count] -= 1
+            arr[i] = count + Min
+            i += 1
+    return arr


Test patch:
diff --git a/tests/test_sort.py b/tests/test_sort.py
index c24e20d..e3fb498 100644
--- a/tests/test_sort.py
+++ b/tests/test_sort.py
@@ -9,6 +9,7 @@ from algorithms.sort import (
     insertion_sort,
     merge_sort,
     pancake_sort,
+    pigeonhole_sort,
     quick_sort,
     selection_sort,
     bucket_sort,
@@ -67,7 +68,10 @@ class TestSuite(unittest.TestCase):
 
     def test_pancake_sort(self):
         self.assertTrue(is_sorted(pancake_sort([1, 3, 2, 5, 65, 23, 57, 1232])))
-
+        
+    def test_pigeonhole_sort(self):
+        self.assertTrue(is_sorted(pigeonhole_sort([1, 5, 65, 23, 57, 1232])))
+        
     def test_quick_sort(self):
         self.assertTrue(is_sorted(quick_sort([1, 3, 2, 5, 65, 23, 57, 1232])))
Repository: keon/algorithms
Pull request #670 (2020-07-01 06:57:10)

add check pangram
add check pangram to strings folder + add test for it


Patch:
diff --git a/README.md b/README.md
index 79c1d08..f2adbaa 100644
--- a/README.md
+++ b/README.md
@@ -322,6 +322,7 @@ If you want to uninstall algorithms, it is as simple as:
     - [judge_circle](algorithms/strings/judge_circle.py)
     - [strong_password](algorithms/strings/strong_password.py)
     - [caesar_cipher](algorithms/strings/caesar_cipher.py)
+    - [check_pangram](algorithms/strings/check_pangram.py
     - [contain_string](algorithms/strings/contain_string.py)
     - [count_binary_substring](algorithms/strings/count_binary_substring.py)
     - [repeat_string](algorithms/strings/repeat_string.py)
diff --git a/algorithms/strings/__init__.py b/algorithms/strings/__init__.py
index 72ea7d7..37c83df 100644
--- a/algorithms/strings/__init__.py
+++ b/algorithms/strings/__init__.py
@@ -25,6 +25,7 @@ from .unique_morse import *
 from .judge_circle import *
 from .strong_password import *
 from .caesar_cipher import *
+from .check_pangram import *
 from .contain_string import *
 from .count_binary_substring import *
 from .repeat_string import *
diff --git a/algorithms/strings/check_pangram.py b/algorithms/strings/check_pangram.py
new file mode 100644
index 0000000..4124257
--- /dev/null
+++ b/algorithms/strings/check_pangram.py
@@ -0,0 +1,10 @@
+"""
+Algorithm that checks if a given string is a pangram or not
+"""
+
+def check_pangram(input_string):
+    alphabet = "abcdefghijklmnopqrstuvwxyz"
+    for ch in alphabet:
+        if ch not in input_string.lower():
+            return False
+    return True 
\ No newline at end of file


Test patch:
diff --git a/tests/test_strings.py b/tests/test_strings.py
index b8750b1..5544086 100644
--- a/tests/test_strings.py
+++ b/tests/test_strings.py
@@ -28,6 +28,7 @@ from algorithms.strings import (
     judge_circle,
     strong_password,
     caesar_cipher,
+    check_pangram,
     contain_string,
     count_binary_substring,
     repeat_string,
@@ -454,6 +455,11 @@ class TestCaesarCipher(unittest.TestCase):
         self.assertEqual("Lipps_Asvph!", caesar_cipher("Hello_World!", 4))
         self.assertEqual("okffng-Qwvb", caesar_cipher("middle-Outz", 2))
 
+class TestCheckPangram(unittest.TestCase):
+    def test_check_pangram(self):
+        self.assertTrue(check_pangram("The quick brown fox jumps over the lazy dog"))
+        self.assertFalse(check_pangram("The quick brown fox"))
+
 
 class TestContainString(unittest.TestCase):
     def test_contain_string(self):
Repository: pypa/pip
Pull request #13394 (2025-05-14 12:29:32)

Requires-Python error message should sort versions numerically, not alphabetically
Implements #13367

Requires-Python is str, so I converted it to a list of Versions and sorted it.



Patch:
diff --git a/news/13367.feature.rst b/news/13367.feature.rst
new file mode 100644
index 000000000..db51d30e8
--- /dev/null
+++ b/news/13367.feature.rst
@@ -0,0 +1,1 @@
+Requires-Python error message now sorts versions numerically.
diff --git a/src/pip/_internal/index/package_finder.py b/src/pip/_internal/index/package_finder.py
index cd195fc36..d942604cf 100644
--- a/src/pip/_internal/index/package_finder.py
+++ b/src/pip/_internal/index/package_finder.py
@@ -248,7 +248,19 @@ class LinkEvaluator:
             ignore_requires_python=self._ignore_requires_python,
         )
         if not supports_python:
-            reason = f"{version} Requires-Python {link.requires_python}"
+            requires_python = link.requires_python
+            if requires_python:
+
+                def get_version_sort_key(v: str) -> tuple[int, ...]:
+                    return tuple(int(s) for s in v.split(".") if s.isdigit())
+
+                requires_python = ",".join(
+                    sorted(
+                        (str(s) for s in specifiers.SpecifierSet(requires_python)),
+                        key=get_version_sort_key,
+                    )
+                )
+            reason = f"{version} Requires-Python {requires_python}"
             return (LinkType.requires_python_mismatch, reason)
 
         logger.debug("Found link %s, version: %s", link, version)


Test patch:
diff --git a/tests/unit/test_index.py b/tests/unit/test_index.py
index 9af722a0c..9b4c881c0 100644
--- a/tests/unit/test_index.py
+++ b/tests/unit/test_index.py
@@ -133,7 +133,7 @@ class TestLinkEvaluator:
                 False,
                 (
                     LinkType.requires_python_mismatch,
-                    "1.12 Requires-Python == 3.6.5",
+                    "1.12 Requires-Python ==3.6.5,!=3.13.3",
                 ),
                 id="requires-python-mismatch",
             ),
@@ -162,7 +162,7 @@ class TestLinkEvaluator:
         )
         link = Link(
             "https://example.com/#egg=twine-1.12",
-            requires_python="== 3.6.5",
+            requires_python="!= 3.13.3, == 3.6.5",
         )
         actual = evaluator.evaluate_link(link)
         assert actual == expected
Repository: pypa/pip
Pull request #12119 (2023-07-01 11:36:48)

Pass revisions options explicitly to mercurial commands


Patch:
diff --git a/news/12119.bugfix.rst b/news/12119.bugfix.rst
new file mode 100644
index 000000000..da8d8b04d
--- /dev/null
+++ b/news/12119.bugfix.rst
@@ -0,0 +1,3 @@
+Pass the ``-r`` flag to mercurial to be explicit that a revision is passed and protect
+against ``hg`` options injection as part of VCS URLs. Users that do not have control on
+VCS URLs passed to pip are advised to upgrade.
diff --git a/src/pip/_internal/vcs/mercurial.py b/src/pip/_internal/vcs/mercurial.py
index 2a005e0af..4595960b5 100644
--- a/src/pip/_internal/vcs/mercurial.py
+++ b/src/pip/_internal/vcs/mercurial.py
@@ -31,7 +31,7 @@ class Mercurial(VersionControl):
 
     @staticmethod
     def get_base_rev_args(rev: str) -> List[str]:
-        return [rev]
+        return ["-r", rev]
 
     def fetch_new(
         self, dest: str, url: HiddenText, rev_options: RevOptions, verbosity: int


Test patch:
diff --git a/tests/unit/test_vcs.py b/tests/unit/test_vcs.py
index 566c88cf0..38daaa0f2 100644
--- a/tests/unit/test_vcs.py
+++ b/tests/unit/test_vcs.py
@@ -66,7 +66,7 @@ def test_rev_options_repr() -> None:
         # First check VCS-specific RevOptions behavior.
         (Bazaar, [], ["-r", "123"], {}),
         (Git, ["HEAD"], ["123"], {}),
-        (Mercurial, [], ["123"], {}),
+        (Mercurial, [], ["-r", "123"], {}),
         (Subversion, [], ["-r", "123"], {}),
         # Test extra_args.  For this, test using a single VersionControl class.
         (