Browse Source

Merge "Choose tracked branch for rebase when submitting"

tags/1.0.0
Jenkins 4 years ago
parent
commit
f4f6674c9c
5 changed files with 286 additions and 13 deletions
  1. 44
    0
      git-review.1
  2. 101
    13
      git_review/cmd.py
  3. 10
    0
      git_review/tests/__init__.py
  4. 73
    0
      git_review/tests/test_git_review.py
  5. 58
    0
      git_review/tests/test_unit.py

+ 44
- 0
git-review.1 View File

@@ -160,6 +160,18 @@ When submitting a change for review, you will usually want it to be based on the
160 160
 Also can be used for
161 161
 .Fl \-compare
162 162
 to skip automatic rebase of fetched reviews.
163
+.It Fl \-track
164
+Choose the branch to submit the change against (and, if
165
+rebasing, to rebase against) from the branch being tracked
166
+(if a branch is being tracked), and set the tracking branch
167
+when downloading a change to point to the remote and branch
168
+against which patches should be submitted.
169
+See gitreview.track configuration.
170
+.It Fl \-no\-track
171
+Ignore any branch being tracked by the current branch,
172
+overriding gitreview.track.
173
+This option is implied by providing a specific branch name
174
+on the command line.
163 175
 .It Fl \-version
164 176
 Print the version number and exit.
165 177
 .El
@@ -199,6 +211,37 @@ This setting determines the default name to use for gerrit remote
199 211
 .It gitreview.branch
200 212
 This setting determines the default branch
201 213
 .Ed
214
+.It gitreview.track
215
+Determines whether to prefer the currently-tracked branch (if any)
216
+and the branch against which the changeset was submitted to Gerrit
217
+(if there is exactly one such branch) to the defaultremote and
218
+defaultbranch for submitting and rebasing against.
219
+If the local topic branch is tracking a remote branch, the remote
220
+and branch that the local topic branch is tracking should be used
221
+for submit and rebase operations, rather than the defaultremote
222
+and defaultbranch.
223
+.Pp
224
+When downloading a patch, creates the local branch to track the
225
+appropriate remote and branch in order to choose that branch by
226
+default when submitting modifications to that changeset.
227
+.Pp
228
+A value of 'true' or 'false' should be specified.
229
+.Bl -tag
230
+.It true
231
+Do prefer the currently-tracked branch (if any) \- equivalent
232
+to setting
233
+.Fl \-track
234
+when submitting changes.
235
+.It false
236
+Ignore tracking branches \- equivalent to setting
237
+.Fl \-no\-track
238
+(the default) or providing an explicit branch name when submitting
239
+changes. This is the default value unless overridden by
240
+.Pa .gitreview
241
+file, and is implied by providing a specific branch name on the
242
+command line.
243
+.El
244
+.Ed
202 245
 .It gitreview.rebase
203 246
 This setting determines whether changes submitted will
204 247
 be rebased to the newest state of the branch.
@@ -278,6 +321,7 @@ project=department/project.git
278 321
 defaultbranch=master
279 322
 defaultremote=review
280 323
 defaultrebase=0
324
+track=0
281 325
 .Ed
282 326
 .Pp
283 327
 When the same option is provided through FILES and CONFIGURATION, the

+ 101
- 13
git_review/cmd.py View File

@@ -56,7 +56,8 @@ CONFIGDIR = os.path.expanduser("~/.config/git-review")
56 56
 GLOBAL_CONFIG = "/etc/git-review/git-review.conf"
57 57
 USER_CONFIG = os.path.join(CONFIGDIR, "git-review.conf")
58 58
 DEFAULTS = dict(scheme='ssh', hostname=False, port=None, project=False,
59
-                branch='master', remote="gerrit", rebase="1")
59
+                branch='master', remote="gerrit", rebase="1",
60
+                track="0")
60 61
 
61 62
 _branch_name = None
62 63
 _has_color = None
@@ -654,6 +655,7 @@ def load_config_file(config_file):
654 655
         'branch': 'defaultbranch',
655 656
         'remote': 'defaultremote',
656 657
         'rebase': 'defaultrebase',
658
+        'track': 'track',
657 659
     }
658 660
     config = {}
659 661
     for config_key, option_name in options.items():
@@ -675,6 +677,39 @@ def update_remote(remote):
675 677
     return True
676 678
 
677 679
 
680
+def parse_tracking(ref=None):
681
+    """Return tracked (remote, branch) of current HEAD or other named
682
+       branch if tracking remote.
683
+    """
684
+    if ref is None:
685
+        ref = run_command_exc(
686
+            SymbolicRefFailed,
687
+            "git", "symbolic-ref", "-q", "HEAD")
688
+    tracked = run_command_exc(
689
+        ForEachRefFailed,
690
+        "git", "for-each-ref", "--format=%(upstream)", ref)
691
+
692
+    # Only on explicitly tracked remote branch do we diverge from default
693
+    if tracked and tracked.startswith('refs/remotes/'):
694
+        return tracked[13:].partition('/')[::2]
695
+
696
+    return None, None
697
+
698
+
699
+def resolve_tracking(remote, branch):
700
+    """Resolve tracked upstream remote/branch if current branch is tracked."""
701
+    tracked_remote, tracked_branch = parse_tracking()
702
+    # tracked_branch will be empty when tracking a local branch
703
+    if tracked_branch:
704
+        if VERBOSE:
705
+            print('Following tracked %s/%s rather than default %s/%s' % (
706
+                  tracked_remote, tracked_branch,
707
+                  remote, branch))
708
+        return tracked_remote, tracked_branch
709
+
710
+    return remote, branch
711
+
712
+
678 713
 def check_remote(branch, remote, scheme, hostname, port, project):
679 714
     """Check that a Gerrit Git remote repo exists, if not, set one."""
680 715
 
@@ -983,6 +1018,26 @@ class ResetHardFailed(CommandFailed):
983 1018
     EXIT_CODE = 66
984 1019
 
985 1020
 
1021
+class SetUpstreamBranchFailed(CommandFailed):
1022
+    "Cannot set upstream to remote branch"
1023
+    EXIT_CODE = 67
1024
+
1025
+
1026
+class SymbolicRefFailed(CommandFailed):
1027
+    "Cannot find symbolic reference"
1028
+    EXIT_CODE = 68
1029
+
1030
+
1031
+class ForEachRefFailed(CommandFailed):
1032
+    "Cannot process symbolic reference"
1033
+    EXIT_CODE = 69
1034
+
1035
+
1036
+class BranchTrackingMismatch(GitReviewException):
1037
+    "Branch exists but is tracking unexpected branch"
1038
+    EXIT_CODE = 70
1039
+
1040
+
986 1041
 def fetch_review(review, masterbranch, remote):
987 1042
     remote_url = get_remote_url(remote)
988 1043
 
@@ -1021,6 +1076,7 @@ def fetch_review(review, masterbranch, remote):
1021 1076
         author = re.sub('\W+', '_', review_info['owner']['name']).lower()
1022 1077
     except KeyError:
1023 1078
         author = 'unknown'
1079
+    remote_branch = review_info['branch']
1024 1080
 
1025 1081
     if patchset_number is None:
1026 1082
         branch_name = "review/%s/%s" % (author, topic)
@@ -1030,10 +1086,10 @@ def fetch_review(review, masterbranch, remote):
1030 1086
     print("Downloading %s from gerrit" % refspec)
1031 1087
     run_command_exc(PatchSetGitFetchFailed,
1032 1088
                     "git", "fetch", remote, refspec)
1033
-    return branch_name
1089
+    return branch_name, remote_branch
1034 1090
 
1035 1091
 
1036
-def checkout_review(branch_name):
1092
+def checkout_review(branch_name, remote, remote_branch):
1037 1093
     """Checkout a newly fetched (FETCH_HEAD) change
1038 1094
        into a branch
1039 1095
     """
@@ -1042,10 +1098,24 @@ def checkout_review(branch_name):
1042 1098
         run_command_exc(CheckoutNewBranchFailed,
1043 1099
                         "git", "checkout", "-b",
1044 1100
                         branch_name, "FETCH_HEAD")
1101
+        # --set-upstream-to is not supported in git 1.7
1102
+        run_command_exc(SetUpstreamBranchFailed,
1103
+                        "git", "branch", "--set-upstream",
1104
+                        branch_name,
1105
+                        '%s/%s' % (remote, remote_branch))
1045 1106
 
1046 1107
     except CheckoutNewBranchFailed as e:
1047 1108
         if re.search("already exists\.?", e.output):
1048
-            print("Branch already exists - reusing")
1109
+            print("Branch %s already exists - reusing" % branch_name)
1110
+            track_remote, track_branch = parse_tracking(
1111
+                ref='refs/heads/' + branch_name)
1112
+            if track_remote and not (track_remote == remote and
1113
+                                     track_branch == remote_branch):
1114
+                print("Branch %s incorrectly tracking %s/%s instead of %s/%s"
1115
+                      % (branch_name,
1116
+                         track_remote, track_branch,
1117
+                         remote, remote_branch))
1118
+                raise BranchTrackingMismatch
1049 1119
             run_command_exc(CheckoutExistingBranchFailed,
1050 1120
                             "git", "checkout", branch_name)
1051 1121
             run_command_exc(ResetHardFailed,
@@ -1102,8 +1172,8 @@ def compare_review(review_spec, branch, remote, rebase=False):
1102 1172
     old_review = build_review_number(review, old_ps)
1103 1173
     new_review = build_review_number(review, new_ps)
1104 1174
 
1105
-    old_branch = fetch_review(old_review, branch, remote)
1106
-    checkout_review(old_branch)
1175
+    old_branch, _ = fetch_review(old_review, branch, remote)
1176
+    checkout_review(old_branch, None, None)
1107 1177
 
1108 1178
     if rebase:
1109 1179
         print('Rebasing %s' % old_branch)
@@ -1112,8 +1182,8 @@ def compare_review(review_spec, branch, remote, rebase=False):
1112 1182
             print('Skipping rebase because of conflicts')
1113 1183
             run_command_exc(CommandFailed, 'git', 'rebase', '--abort')
1114 1184
 
1115
-    new_branch = fetch_review(new_review, branch, remote)
1116
-    checkout_review(new_branch)
1185
+    new_branch, remote_branch = fetch_review(new_review, branch, remote)
1186
+    checkout_review(new_branch, remote, remote_branch)
1117 1187
 
1118 1188
     if rebase:
1119 1189
         print('Rebasing also %s' % new_branch)
@@ -1187,6 +1257,14 @@ def _main():
1187 1257
                               action="store_true",
1188 1258
                               help="Force rebase even when not needed.")
1189 1259
 
1260
+    track_group = parser.add_mutually_exclusive_group()
1261
+    track_group.add_argument("--track", dest="track",
1262
+                             action="store_true",
1263
+                             help="Use tracked branch as default.")
1264
+    track_group.add_argument("--no-track", dest="track",
1265
+                             action="store_false",
1266
+                             help="Ignore tracked branch.")
1267
+
1190 1268
     fetch = parser.add_mutually_exclusive_group()
1191 1269
     fetch.set_defaults(download=False, compare=False, cherrypickcommit=False,
1192 1270
                        cherrypickindicate=False, cherrypickonly=False)
@@ -1274,8 +1352,8 @@ def _main():
1274 1352
     else:
1275 1353
         no_git_dir = False
1276 1354
         config = Config(os.path.join(top_dir, ".gitreview"))
1277
-        parser.set_defaults(branch=config['branch'],
1278
-                            rebase=convert_bool(config['rebase']),
1355
+        parser.set_defaults(rebase=convert_bool(config['rebase']),
1356
+                            track=convert_bool(config['track']),
1279 1357
                             remote=config['remote'])
1280 1358
     options = parser.parse_args()
1281 1359
     if no_git_dir:
@@ -1285,7 +1363,13 @@ def _main():
1285 1363
         print(COPYRIGHT)
1286 1364
         sys.exit(0)
1287 1365
 
1288
-    branch = options.branch
1366
+    if options.branch is None:
1367
+        branch = config['branch']
1368
+    else:
1369
+        # explicitly-specified branch on command line overrides options.track
1370
+        branch = options.branch
1371
+        options.track = False
1372
+
1289 1373
     global VERBOSE
1290 1374
     global UPDATE
1291 1375
     VERBOSE = options.verbose
@@ -1294,6 +1378,9 @@ def _main():
1294 1378
     yes = options.yes
1295 1379
     status = 0
1296 1380
 
1381
+    if options.track:
1382
+        remote, branch = resolve_tracking(remote, branch)
1383
+
1297 1384
     check_remote(branch, remote, config['scheme'],
1298 1385
                  config['hostname'], config['port'], config['project'])
1299 1386
 
@@ -1305,9 +1392,10 @@ def _main():
1305 1392
             compare_review(options.changeidentifier,
1306 1393
                            branch, remote, options.rebase)
1307 1394
             return
1308
-        local_branch = fetch_review(options.changeidentifier, branch, remote)
1395
+        local_branch, remote_branch = fetch_review(options.changeidentifier,
1396
+                                                   branch, remote)
1309 1397
         if options.download:
1310
-            checkout_review(local_branch)
1398
+            checkout_review(local_branch, remote, remote_branch)
1311 1399
         else:
1312 1400
             if options.cherrypickcommit:
1313 1401
                 cherrypick_review()

+ 10
- 0
git_review/tests/__init__.py View File

@@ -239,6 +239,16 @@ class BaseGitReviewTestCase(testtools.TestCase, GerritHelpers):
239 239
         self._run_git('add', file_)
240 240
         self._run_git('commit', '-m', commit_message)
241 241
 
242
+    def _simple_amend(self, change_text, file_=None):
243
+        """Helper method to amend existing commit with change."""
244
+        if file_ is None:
245
+            file_ = self._dir('test', 'test_file_new.txt')
246
+        utils.write_to_file(file_, change_text.encode())
247
+        self._run_git('add', file_)
248
+        # cannot use --no-edit because it does not exist in older git
249
+        message = self._run_git('log', '-1', '--format=%s\n\n%b')
250
+        self._run_git('commit', '--amend', '-m', message)
251
+
242 252
     def _configure_ssh(self, ssh_addr, ssh_port):
243 253
         """Setup ssh and scp to run with special options."""
244 254
 

+ 73
- 0
git_review/tests/test_git_review.py View File

@@ -90,6 +90,12 @@ class GitReviewTestCase(tests.BaseGitReviewTestCase):
90 90
         self.assertNotIn('test commit message',
91 91
                          self._run_git('show', 'HEAD^1'))
92 92
 
93
+        # and branch is tracking
94
+        head = self._run_git('symbolic-ref', '-q', 'HEAD')
95
+        self.assertIn(
96
+            'refs/remotes/gerrit/master',
97
+            self._run_git("for-each-ref", "--format='%(upstream)'", head))
98
+
93 99
     def test_multiple_changes(self):
94 100
         """Test git-review asks about multiple changes.
95 101
 
@@ -160,6 +166,73 @@ class GitReviewTestCase(tests.BaseGitReviewTestCase):
160 166
                       review_res)
161 167
         self.assertEqual(self._run_git('rev-parse', 'HEAD^1'), head_1)
162 168
 
169
+    def test_uploads_with_nondefault_rebase(self):
170
+        """Test changes rebase against correct branches."""
171
+        # prepare maintenance branch that is behind master
172
+        self._create_gitreview_file(track='true',
173
+                                    defaultremote='origin')
174
+        self._run_git('add', '.gitreview')
175
+        self._run_git('commit', '-m', 'track=true.')
176
+        self._simple_change('diverge master from maint',
177
+                            'no conflict',
178
+                            self._dir('test', 'test_file_to_diverge.txt'))
179
+        self._run_git('push', 'origin', 'master')
180
+        self._run_git('push', 'origin', 'master', 'master:other')
181
+        self._run_git_review('-s')
182
+        head_1 = self._run_git('rev-parse', 'HEAD^1')
183
+        self._run_gerrit_cli('create-branch',
184
+                             'test/test_project',
185
+                             'maint', head_1)
186
+        self._run_git('fetch')
187
+
188
+        br_out = self._run_git('checkout',
189
+                               '-b', 'test_branch', 'origin/maint')
190
+        expected_track = 'Branch test_branch set up to track remote' + \
191
+                         ' branch maint from origin.'
192
+        self.assertIn(expected_track, br_out)
193
+        branches = self._run_git('branch', '-a')
194
+        expected_branch = '* test_branch'
195
+        observed = branches.split('\n')
196
+        self.assertIn(expected_branch, observed)
197
+
198
+        self._simple_change('some new message',
199
+                            'just another file (no conflict)',
200
+                            self._dir('test', 'new_tracked_test_file.txt'))
201
+        change_id = self._run_git('log', '-1').split()[-1]
202
+
203
+        review_res = self._run_git_review('-v')
204
+        # no rebase needed; if it breaks it would try to rebase to master
205
+        self.assertNotIn("Running: git rebase -p -i remotes/origin/master",
206
+                         review_res)
207
+        # Don't need to query gerrit for the branch as the second half
208
+        # of this test will work only if the branch was correctly
209
+        # stored in gerrit
210
+
211
+        # delete branch locally
212
+        self._run_git('checkout', 'master')
213
+        self._run_git('branch', '-D', 'test_branch')
214
+
215
+        # download, amend, submit
216
+        self._run_git_review('-d', change_id)
217
+        self._simple_amend('just another file (no conflict)',
218
+                           self._dir('test', 'new_tracked_test_file_2.txt'))
219
+        new_change_id = self._run_git('log', '-1').split()[-1]
220
+        self.assertEqual(change_id, new_change_id)
221
+        review_res = self._run_git_review('-v')
222
+        # caused the right thing to happen
223
+        self.assertIn("Running: git rebase -p -i remotes/origin/maint",
224
+                      review_res)
225
+
226
+        # track different branch than expected in changeset
227
+        branch = self._run_git('rev-parse', '--abbrev-ref', 'HEAD')
228
+        self._run_git('branch',
229
+                      '--set-upstream',
230
+                      branch,
231
+                      'remotes/origin/other')
232
+        self.assertRaises(
233
+            Exception,  # cmd.BranchTrackingMismatch inside
234
+            self._run_git_review, '-d', change_id)
235
+
163 236
     def test_no_rebase_check(self):
164 237
         """Test -R causes a change to be uploaded without rebase checking."""
165 238
         self._run_git_review('-s')

+ 58
- 0
git_review/tests/test_unit.py View File

@@ -176,6 +176,48 @@ password=pass
176 176
 """
177 177
 
178 178
 
179
+class ResolveTrackingUnitTest(testtools.TestCase):
180
+    """Class for testing resolve_tracking."""
181
+    def setUp(self):
182
+        testtools.TestCase.setUp(self)
183
+        patcher = mock.patch('git_review.cmd.run_command_exc')
184
+        self.addCleanup(patcher.stop)
185
+        self.run_command_exc = patcher.start()
186
+
187
+    def test_track_local_branch(self):
188
+        'Test that local tracked branch is not followed.'
189
+        self.run_command_exc.side_effect = [
190
+            '',
191
+            'refs/heads/other/branch',
192
+        ]
193
+        self.assertEqual(cmd.resolve_tracking(u'remote', u'rbranch'),
194
+                         (u'remote', u'rbranch'))
195
+
196
+    def test_track_untracked_branch(self):
197
+        'Test that local untracked branch is not followed.'
198
+        self.run_command_exc.side_effect = [
199
+            '',
200
+            '',
201
+        ]
202
+        self.assertEqual(cmd.resolve_tracking(u'remote', u'rbranch'),
203
+                         (u'remote', u'rbranch'))
204
+
205
+    def test_track_remote_branch(self):
206
+        'Test that remote tracked branch is followed.'
207
+        self.run_command_exc.side_effect = [
208
+            '',
209
+            'refs/remotes/other/branch',
210
+        ]
211
+        self.assertEqual(cmd.resolve_tracking(u'remote', u'rbranch'),
212
+                         (u'other', u'branch'))
213
+
214
+    def test_track_git_error(self):
215
+        'Test that local tracked branch is not followed.'
216
+        self.run_command_exc.side_effect = [cmd.CommandFailed(1, '', [], {})]
217
+        self.assertRaises(cmd.CommandFailed,
218
+                          cmd.resolve_tracking, u'remote', u'rbranch')
219
+
220
+
179 221
 class GitReviewUnitTest(testtools.TestCase):
180 222
     """Class for misc unit tests."""
181 223
 
@@ -236,3 +278,19 @@ class GitReviewUnitTest(testtools.TestCase):
236 278
                                          stdin='url=%s' % url)
237 279
         calls = [mock.call(url), mock.call(url, auth=('user', 'pass'))]
238 280
         mock_get.assert_has_calls(calls)
281
+
282
+    @mock.patch('sys.argv', ['argv0', '--track', 'branch'])
283
+    @mock.patch('git_review.cmd.check_remote')
284
+    @mock.patch('git_review.cmd.resolve_tracking')
285
+    def test_command_line_no_track(self, resolve_tracking, check_remote):
286
+        check_remote.side_effect = Exception()
287
+        self.assertRaises(Exception, cmd._main)
288
+        self.assertFalse(resolve_tracking.called)
289
+
290
+    @mock.patch('sys.argv', ['argv0', '--track'])
291
+    @mock.patch('git_review.cmd.check_remote')
292
+    @mock.patch('git_review.cmd.resolve_tracking')
293
+    def test_track(self, resolve_tracking, check_remote):
294
+        check_remote.side_effect = Exception()
295
+        self.assertRaises(Exception, cmd._main)
296
+        self.assertTrue(resolve_tracking.called)

Loading…
Cancel
Save