Repository: VirtusLab/git-machete
Pull request #1352 (2024-10-12 09:29:03)

Provide a GitHub PR title from `.git/info/title` file
Solves https://github.com/VirtusLab/git-machete/issues/1351 (not sure if having a linked issue is preferred or not, created one just in case)

Similar to the functionality for adding a PR description from `.git/info/description`, it would be useful to be able to set the PR title from a `.git/info/title` file.

This is a suggestion for an alternative method to the `--title` CLI parameter, which should take precedence over this method.

## Expected behavior

Given that
- `.git/info/title` contains a title, e.g. "Add New Feature"
- `.git/info/description` contains a description, e.g. "# Summary\n..."

When we invoke
```
git machete github create-pr
```

Then a pull request is created with the title and description set.

russellyeo: This was tested by creating and merging a PR in my fork of the repository here - https://github.com/russellyeo/git-machete/pull/2
PawelLipski: Nice! complete with tests and docs 🥰 I'll take a closer look on Sunday/Monday
PawelLipski: Hmmm one general consideration... I'd say that's it's rather rare for people to have a constant PR title that they'd like to reuse across pull requests (and `.git/info/...` or other such files like in `.github/...` are best suited for such repetitive contents). This is unlike with PR description, which is very likely to be reused across PRs. What is your use case/what is the PR title that you're reusing? 🤔 
russellyeo: Hey @PawelLipski, thanks for taking a look at this. I also want to thank you for the excellent tool - I now can't imagine working without it.

> Hmmm one general consideration... I'd say that's it's rather rare for people to have a constant PR title that they'd like to reuse across pull requests (and `.git/info/...` or other such files like in `.github/...` are best suited for such repetitive contents). This is unlike with PR description, which is very likely to be reused across PRs. What is your use case/what is the PR title that you're reusing? 🤔

Good point. Perhaps my workflow is a little unusual - I use a git alias which opens these files one-by-one to gather my input and then create the PR:

```sh
[alias]
    create-pr = !code --wait .git/info/description && code --wait .git/info/reviewers && git machete github create-pr
```
(Invoked with `git create-pr`)

This change would fit nicely into this workflow. It could likely be achieved with some scripting and the `--title` PR, but I thought it would be a nice opportunity to follow the existing pattern. Writing to a file also lends itself well to scripting. For example, I might set up a script to automate converting the branch name into a nicely formatted title.

PawelLipski: >  I also want to thank you for the excellent tool - I now can't imagine working without it.
Thank you!

> Good point. Perhaps my workflow is a little unusual - I use a git alias which opens these files one-by-one to gather my input and then create the PR

Okay, I see... this still looks like a quite unusual approach 😅 As you can imagine, I'm pretty concerned about bloating the interface of git-machete too much, given that there's a minor release (i.e. new features) pretty much every month and [docs are already huuuge](https://git-machete.readthedocs.io/en/stable/) 🫠

May I suggest one of the following:

1. Keep the feature as-is, but skip it from docs. This way it'll cater for your specific workflow, without the extra perceived complexity to `create-{pr,mr}` (esp. important for fresh users). There're already some undocumented features that only I am likely to ever use, like [`GIT_MACHETE_MEASURE_COMMAND_TIME` env var](https://github.com/VirtusLab/git-machete/blob/v3.30.0/git_machete/utils.py#L23).
2. Alternatively, instead of using `.git/info/title` which suggests a static title to be used for all PRs (which is rather impractical and hence potentially confusing for the other users), allow for passing @-prefixed file name as title: `--title=@foo.txt`. This is a pattern not found in git-machete yet, but otherwise pretty well-known from tools like `curl`. This will be less confusing as it suggests that title isn't static but rather dynamically generated.

WDYT?
russellyeo: @PawelLipski 

Thanks for the suggestions, both options seem like a good compromise. I'm happy with either!  I've reverted the changes to the docs as the simplest route (option 1). I'm also open to discarding all changes altogether, just let me know what your preference is

Patch:
diff --git a/git_machete/client.py b/git_machete/client.py
index 1beb5bd..2689ecd 100644
--- a/git_machete/client.py
+++ b/git_machete/client.py
@@ -2756,8 +2756,12 @@ class MacheteClient:
         fork_point = self.fork_point(head, use_overrides=True)
         commits: List[GitLogEntry] = self.__git.get_commits_between(fork_point, head)
 
+        pr_title_file_path = self.__git.get_main_git_subpath('info', 'title')
+        is_pr_title_file = os.path.isfile(pr_title_file_path)
         if opt_title:
             title = opt_title
+        elif is_pr_title_file:
+            title = utils.slurp_file(pr_title_file_path)
         else:
             # git-machete can still see an empty range of unique commits (e.g. in case of yellow edge)
             # even though code hosting may see a non-empty range.


Test patch:
diff --git a/tests/test_github_create_pr.py b/tests/test_github_create_pr.py
index 1e6b240..f1f7255 100644
--- a/tests/test_github_create_pr.py
+++ b/tests/test_github_create_pr.py
@@ -954,3 +954,35 @@ class TestGitHubCreatePR(BaseTest):
             Creating a PR from feature to develop... OK, see www.github.com
             """
         )
+
+    def test_github_create_pr_with_title_from_file(self, mocker: MockerFixture) -> None:
+        self.patch_symbol(mocker, 'git_machete.code_hosting.OrganizationAndRepository.from_url', mock_from_url)
+        self.patch_symbol(mocker, 'git_machete.github.GitHubToken.for_domain', mock_github_token_for_domain_none)
+        github_api_state = MockGitHubAPIState.with_prs()
+        self.patch_symbol(mocker, 'urllib.request.urlopen', mock_urlopen(github_api_state))
+
+        (
+            self.repo_sandbox
+            .new_branch("develop").commit("Some commit").push()
+            .new_branch("feature").commit("Add feature").push()
+        )
+
+        rewrite_branch_layout_file("develop\n\tfeature")
+
+        pr_title = "Feature Implementation"
+        self.repo_sandbox.write_to_file(".git/info/title", pr_title)
+
+        launch_command("github", "create-pr")
+
+        pr = github_api_state.get_pull_by_number(1)
+        assert pr is not None
+        assert pr['title'] == pr_title
+
+        assert_success(
+            ['status'],
+            """
+            develop
+            |
+            o-feature *  PR #1 (some_other_user)
+            """,
+        )
diff --git a/tests/test_gitlab_create_mr.py b/tests/test_gitlab_create_mr.py
index 30e8d23..941a72b 100644
--- a/tests/test_gitlab_create_mr.py
+++ b/tests/test_gitlab_create_mr.py
@@ -934,3 +934,35 @@ class TestGitLabCreateMR(BaseTest):
             Creating a MR from feature to develop... OK, see www.gitlab.com
             """
         )
+
+    def test_gitlab_create_mr_with_title_from_file(self, mocker: MockerFixture) -> None:
+        self.patch_symbol(mocker, 'git_machete.code_hosting.OrganizationAndRepository.from_url', mock_from_url)
+        self.patch_symbol(mocker, 'git_machete.gitlab.GitLabToken.for_domain', mock_gitlab_token_for_domain_none)
+        gitlab_api_state = MockGitLabAPIState.with_mrs()
+        self.patch_symbol(mocker, 'urllib.request.urlopen', mock_urlopen(gitlab_api_state))
+
+        (
+            self.repo_sandbox
+            .new_branch("develop").commit("Some commit").push()
+            .new_branch("feature").commit("Add feature").push()
+        )
+
+        rewrite_branch_layout_file("develop\n\tfeature")
+
+        pr_title = "Feature Implementation"
+        self.repo_sandbox.write_to_file(".git/info/title", pr_title)
+
+        launch_command("gitlab", "create-mr")
+
+        pr = gitlab_api_state.get_mr_by_number(1)
+        assert pr is not None
+        assert pr['title'] == pr_title
+
+        assert_success(
+            ['status'],
+            """
+            develop
+            |
+            o-feature *  MR !1 (some_other_user)
+            """,
+        )