#!/usr/bin/env python # # log_tests.py: testing "svn log" # # Subversion is a tool for revision control. # See http://subversion.apache.org for more information. # # ==================================================================== # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. ###################################################################### # General modules import re, os, sys # Our testing module import svntest from svntest import wc from svntest.main import server_has_mergeinfo from svntest.main import SVN_PROP_MERGEINFO from merge_tests import set_up_branch from diff_tests import make_diff_header, make_no_diff_deleted_header # (abbreviation) Skip = svntest.testcase.Skip_deco SkipUnless = svntest.testcase.SkipUnless_deco XFail = svntest.testcase.XFail_deco Issues = svntest.testcase.Issues_deco Issue = svntest.testcase.Issue_deco Wimp = svntest.testcase.Wimp_deco exp_noop_up_out = svntest.actions.expected_noop_update_output ###################################################################### # # The Plan: # # Get a repository, commit about 6 or 7 revisions to it, each # involving different kinds of operations. Make sure to have some # add, del, mv, cp, as well as file modifications, and make sure that # some files are modified more than once. # # Give each commit a recognizable log message. Test all combinations # of -r options, including none. Then test with -v, which will # (presumably) show changed paths as well. # ###################################################################### ###################################################################### # Globals # # These variables are set by guarantee_repos_and_wc(). max_revision = 0 # Highest revision in the repos # What separates log msgs from one another in raw log output. msg_separator = '------------------------------------' \ + '------------------------------------\n' # (abbreviation) Item = svntest.wc.StateItem ###################################################################### # Utilities # def guarantee_repos_and_wc(sbox): "Make a repos and wc, commit max_revision revs." global max_revision sbox.build() wc_path = sbox.wc_dir msg_file=os.path.join(sbox.repo_dir, 'log-msg') msg_file=os.path.abspath(msg_file) # Now we have a repos and wc at revision 1. was_cwd = os.getcwd() os.chdir(wc_path) # Set up the paths we'll be using most often. iota_path = os.path.join('iota') mu_path = os.path.join('A', 'mu') B_path = os.path.join('A', 'B') omega_path = os.path.join('A', 'D', 'H', 'omega') pi_path = os.path.join('A', 'D', 'G', 'pi') rho_path = os.path.join('A', 'D', 'G', 'rho') alpha_path = os.path.join('A', 'B', 'E', 'alpha') beta_path = os.path.join('A', 'B', 'E', 'beta') psi_path = os.path.join('A', 'D', 'H', 'psi') epsilon_path = os.path.join('A', 'C', 'epsilon') # Do a varied bunch of commits. No copies yet, we'll wait till Ben # is done for that. # Revision 2: edit iota msg=""" Log message for revision 2 but with multiple lines to test the code""" svntest.main.file_write(msg_file, msg) svntest.main.file_append(iota_path, "2") svntest.main.run_svn(None, 'ci', '-F', msg_file) svntest.main.run_svn(None, 'up') # Revision 3: edit A/D/H/omega, A/D/G/pi, A/D/G/rho, and A/B/E/alpha svntest.main.file_append(omega_path, "3") svntest.main.file_append(pi_path, "3") svntest.main.file_append(rho_path, "3") svntest.main.file_append(alpha_path, "3") svntest.main.run_svn(None, 'ci', '-m', "Log message for revision 3") svntest.main.run_svn(None, 'up') # Revision 4: edit iota again, add A/C/epsilon msg=""" Log message for revision 4 but with multiple lines to test the code""" svntest.main.file_write(msg_file, msg) svntest.main.file_append(iota_path, "4") svntest.main.file_append(epsilon_path, "4") svntest.main.run_svn(None, 'add', epsilon_path) svntest.main.run_svn(None, 'ci', '-F', msg_file) svntest.main.run_svn(None, 'up') # Revision 5: edit A/C/epsilon, delete A/D/G/rho svntest.main.file_append(epsilon_path, "5") svntest.main.run_svn(None, 'rm', rho_path) svntest.main.run_svn(None, 'ci', '-m', "Log message for revision 5") svntest.main.run_svn(None, 'up') # Revision 6: prop change on A/B, edit A/D/H/psi msg=""" Log message for revision 6 but with multiple lines to test the code""" svntest.main.file_write(msg_file, msg) svntest.main.run_svn(None, 'ps', 'blue', 'azul', B_path) svntest.main.file_append(psi_path, "6") svntest.main.run_svn(None, 'ci', '-F', msg_file) svntest.main.run_svn(None, 'up') # Revision 7: edit A/mu, prop change on A/mu svntest.main.file_append(mu_path, "7") svntest.main.run_svn(None, 'ps', 'red', 'burgundy', mu_path) svntest.main.run_svn(None, 'ci', '-m', "Log message for revision 7") svntest.main.run_svn(None, 'up') # Revision 8: edit iota yet again, re-add A/D/G/rho msg=""" Log message for revision 8 but with multiple lines to test the code""" svntest.main.file_write(msg_file, msg) svntest.main.file_append(iota_path, "8") svntest.main.file_append(rho_path, "88") # More than one char so libmagic # treats it as text. svntest.main.run_svn(None, 'add', rho_path) svntest.main.run_svn(None, 'ci', '-F', msg_file) svntest.main.run_svn(None, 'up') # Revision 9: edit A/B/E/beta, delete A/B/E/alpha svntest.main.file_append(beta_path, "9") svntest.main.run_svn(None, 'rm', alpha_path) svntest.main.run_svn(None, 'ci', '-m', "Log message for revision 9") svntest.main.run_svn(None, 'up') max_revision = 9 # Restore. os.chdir(was_cwd) # Let's run 'svn status' and make sure the working copy looks # exactly the way we think it should. Start with a generic # greek-tree-list, where every local and repos revision is at 9. expected_status = svntest.actions.get_virginal_state(wc_path, 9) expected_status.remove('A/B/E/alpha') expected_status.add({ 'A/C/epsilon' : Item(status=' ', wc_rev=9), }) # props exist on A/B and A/mu expected_status.tweak('A/B', 'A/mu', status=' ') # Run 'svn st -uv' and compare the actual results with our tree. svntest.actions.run_and_verify_status(wc_path, expected_status) def merge_history_repos(sbox): """Make a repos with varied and interesting merge history, similar to the repos found at: log_tests_data/merge_history_dump.png""" upsilon_path = os.path.join('A', 'upsilon') omicron_path = os.path.join('blocked', 'omicron') branch_a = os.path.join('branches', 'a') branch_b = os.path.join('branches', 'b') branch_c = os.path.join('branches', 'c') # Create an empty repository - r0 svntest.main.safe_rmtree(sbox.repo_dir, 1) svntest.main.safe_rmtree(sbox.wc_dir, 1) svntest.main.create_repos(sbox.repo_dir) svntest.actions.run_and_verify_svn(None, None, [], "co", sbox.repo_url, sbox.wc_dir) was_cwd = os.getcwd() os.chdir(sbox.wc_dir) # Create trunk/tags/branches - r1 svntest.main.run_svn(None, 'mkdir', 'trunk') svntest.main.run_svn(None, 'mkdir', 'tags') svntest.main.run_svn(None, 'mkdir', 'branches') svntest.main.run_svn(None, 'ci', '-m', 'Add trunk/tags/branches structure.') # Import greek tree to trunk - r2 svntest.main.greek_state.write_to_disk('trunk') svntest.main.run_svn(None, 'add', os.path.join('trunk', 'A'), os.path.join('trunk', 'iota')) svntest.main.run_svn(None, 'ci', '-m', 'Import greek tree into trunk.') # Update from the repository to avoid a mix-rev working copy svntest.main.run_svn(None, 'up') # Create a branch - r3 svntest.main.run_svn(None, 'cp', 'trunk', branch_a) svntest.main.run_svn(None, 'ci', '-m', 'Create branches/a from trunk.', '--username', svntest.main.wc_author2) # Some changes on the branch - r4 svntest.main.file_append_binary(os.path.join(branch_a, 'iota'), "'A' has changed a bit.\n") svntest.main.file_append_binary(os.path.join(branch_a, 'A', 'mu'), "Don't forget to look at 'upsilon', too.") svntest.main.file_write(os.path.join(branch_a, upsilon_path), "This is the file 'upsilon'.\n", "wb") svntest.main.run_svn(None, 'add', os.path.join(branch_a, upsilon_path)) svntest.main.run_svn(None, 'ci', '-m', "Add the file 'upsilon', and change some other files.") # Create another branch - r5 svntest.main.run_svn(None, 'cp', 'trunk', branch_c) svntest.main.run_svn(None, 'ci', '-m', 'Create branches/c from trunk.', '--username', svntest.main.wc_author2) # Do some mergeing - r6 # From branch_a to trunk: add 'upsilon' and modify 'iota' and 'mu'. # # Mergeinfo changes on /trunk: # Merged /trunk:r2 # Merged /branches/a:r3-5 os.chdir('trunk') svntest.main.run_svn(None, 'merge', os.path.join('..', branch_a) + '@HEAD') svntest.main.run_svn(None, 'ci', '-m', 'Merged branches/a to trunk.', '--username', svntest.main.wc_author2) os.chdir('..') # Add 'blocked/omicron' to branches/a - r7 svntest.main.run_svn(None, 'mkdir', os.path.join(branch_a, 'blocked')) svntest.main.file_write(os.path.join(branch_a, omicron_path), "This is the file 'omicron'.\n") svntest.main.run_svn(None, 'add', os.path.join(branch_a, omicron_path)) svntest.main.run_svn(None, 'ci', '-m', "Add omicron to branches/a. " + "It will be blocked from merging in r8.") # Block r7 from being merged to trunk - r8 # # Mergeinfo changes on /trunk: # Merged /branches/a:r7 os.chdir('trunk') svntest.main.run_svn(None, 'merge', '--allow-mixed-revisions', '--record-only', '-r6:7', os.path.join('..', branch_a)) svntest.main.run_svn(None, 'ci', '-m', "Block r7 from merging to trunk.", '--username', svntest.main.wc_author2) os.chdir('..') # Wording change in mu - r9 svntest.main.file_write(os.path.join('trunk', 'A', 'mu'), "This is the file 'mu'.\n" + "Don't forget to look at 'upsilon', as well.", "wb") svntest.main.run_svn(None, 'ci', '-m', "Wording change in mu.") # Update from the repository to avoid a mix-rev working copy svntest.main.run_svn(None, 'up') # Create another branch - r10 svntest.main.run_svn(None, 'cp', 'trunk', branch_b) svntest.main.run_svn(None, 'ci', '-m', "Create branches/b from trunk", '--username', svntest.main.wc_author2) # Add another file, make some changes on branches/a - r11 svntest.main.file_append_binary(os.path.join(branch_a, upsilon_path), "There is also the file 'xi'.") svntest.main.file_write(os.path.join(branch_a, 'A', 'xi'), "This is the file 'xi'.\n", "wb") svntest.main.run_svn(None, 'add', os.path.join(branch_a, 'A', 'xi')) svntest.main.file_write(os.path.join(branch_a, 'iota'), "This is the file 'iota'.\n" + "'A' has changed a bit, with 'upsilon', and 'xi'.", "wb") svntest.main.run_svn(None, 'ci', '-m', "Added 'xi' to branches/a, made a few other changes.") # Merge branches/a to branches/b - r12 # # Mergeinfo changes on /branches/b: # Merged /branches/a:r6,8-11 os.chdir(branch_b) svntest.main.run_svn(None, 'merge', os.path.join('..', 'a') + '@HEAD') svntest.main.run_svn(None, 'ci', '-m', "Merged branches/a to branches/b.", '--username', svntest.main.wc_author2) os.chdir(os.path.join('..', '..')) # More wording changes - r13 svntest.main.file_append_binary(os.path.join(branch_b, 'A', 'D', 'gamma'), "Watch out for the rays!") svntest.main.run_svn(None, 'ci', '-m', "Modify 'gamma' on branches/b.") # More merging - r14 # # Mergeinfo changes on /trunk: # Reverse-merged /trunk:r2 # Merged /trunk:r3-9 # Merged /branches/a:r6,8-11 # Merged /branches/b:r10-13 os.chdir('trunk') svntest.main.run_svn(None, 'merge', os.path.join('..', branch_b) + '@HEAD') svntest.main.run_svn(None, 'ci', '-m', "Merged branches/b to trunk.", '--username', svntest.main.wc_author2) os.chdir('..') # Even more merging - r15 # # Mergeinfo changes on /branches/c: # Merged /trunk:r3-14 # Merged /branches/a:r3-11 # Merged /branches/b:r10-13 os.chdir(branch_c) svntest.main.run_svn(None, 'merge', os.path.join('..', '..', 'trunk') + '@HEAD') svntest.main.run_svn(None, 'ci', '-m', "Bring branches/c up to date with trunk.", '--username', svntest.main.wc_author2) os.chdir(os.path.join('..', '..')) # Modify a file on branches/c - r16 svntest.main.file_append_binary(os.path.join(branch_c, 'A', 'mu'), "\nThis is yet more content in 'mu'.") svntest.main.run_svn(None, 'ci', '-m', "Modify 'mu' on branches/c.") # Merge branches/c to trunk, which produces a conflict - r17 # # Mergeinfo changes on /trunk: # Merged /trunk:r2 # Merged /branches/c:r3-16 os.chdir('trunk') svntest.main.run_svn(None, 'merge', '--allow-mixed-revisions', os.path.join('..', branch_c) + '@HEAD') svntest.main.file_write(os.path.join('A', 'mu'), "This is the file 'mu'.\n" + "Don't forget to look at 'upsilon', as well.\n" + "This is yet more content in 'mu'.", "wb") # Resolve conflicts, and commit svntest.actions.run_and_verify_resolved([os.path.join('A', 'mu'), os.path.join('A', 'xi'), os.path.join('A', 'upsilon')]) svntest.main.run_svn(None, 'ci', '-m', "Merge branches/c to trunk, " + "resolving a conflict in 'mu'.", '--username', svntest.main.wc_author2) os.chdir('..') # Restore working directory os.chdir(was_cwd) # For errors seen while parsing log data. class SVNLogParseError(Exception): pass def parse_log_output(log_lines, with_diffs=False): """Return a log chain derived from LOG_LINES. A log chain is a list of hashes; each hash represents one log message, in the order it appears in LOG_LINES (the first log message in the data is also the first element of the list, and so on). Each hash contains the following keys/values: 'revision' ===> number 'author' ===> string 'date' ===> string 'msg' ===> string (the log message itself) 'lines' ===> number (so that it may be checked against rev) If LOG_LINES contains changed-path information, then the hash also contains 'paths' ===> list of tuples of the form (X, PATH), where X is the first column of verbose output, and PATH is the affected path. If LOG_LINES contains merge result information, then the hash also contains 'merges' ===> list of forward-merging revisions that resulted in this log being part of the list of messages. 'reverse_merges' ===> list of reverse-merging revisions that resulted in this log being part of the list of messages. If LOG_LINES contains diffs and WITH_DIFFS=True, then the hash also contains 'diff_lines' ===> list of strings (diffs) """ # Here's some log output to look at while writing this function: # ------------------------------------------------------------------------ # r5 | kfogel | Tue 6 Nov 2001 17:18:19 | 1 line # # Log message for revision 5. # ------------------------------------------------------------------------ # r4 | kfogel | Tue 6 Nov 2001 17:18:18 | 3 lines # # Log message for revision 4 # but with multiple lines # to test the code. # ------------------------------------------------------------------------ # r3 | kfogel | Tue 6 Nov 2001 17:18:17 | 1 line # # Log message for revision 3. # ------------------------------------------------------------------------ # r2 | kfogel | Tue 6 Nov 2001 17:18:16 | 3 lines # # Log message for revision 2 # but with multiple lines # to test the code. # ------------------------------------------------------------------------ # r1 | foo | Tue 6 Nov 2001 15:27:57 | 1 line # # Log message for revision 1. # ------------------------------------------------------------------------ # Regular expression to match the header line of a log message, with # these groups: (revision number), (author), (date), (num lines). header_re = re.compile('^r([0-9]+) \| ' \ + '([^|]*) \| ([^|]*) \| ([0-9]+) lines?') # The log chain to return. chain = [] # Filter debug lines from the output. log_lines = [line for line in log_lines if not line.startswith('DBG:')] this_item = None while True: try: this_line = log_lines.pop(0) except IndexError: return chain match = header_re.search(this_line) if match and match.groups(): is_result = 0 is_result_reverse = 0 this_item = {} this_item['revision'] = int(match.group(1)) this_item['author'] = match.group(2) this_item['date'] = match.group(3) lines = int(match.group(4)) this_item['lines'] = lines # Parse verbose output, starting with "Changed paths" next_line = log_lines.pop(0) if next_line.strip() == 'Changed paths:': paths = [] path_line = log_lines.pop(0).strip() # Stop on either a blank line or a "(Reverse) Merged via: ..." line while (path_line != '' and path_line[0:6] != 'Merged' and path_line[0:14] != 'Reverse merged'): paths.append( (path_line[0], path_line[2:]) ) path_line = log_lines.pop(0).strip() this_item['paths'] = paths if path_line[0:6] == 'Merged': is_result = 1 result_line = path_line elif path_line[0:14] == 'Reverse merged': is_result_reverse = 1 result_line = path_line elif next_line[0:6] == 'Merged': is_result = 1 result_line = next_line.strip() elif next_line[0:14] == 'Reverse merged': is_result_reverse = 1 result_line = next_line.strip() # Parse output of "Merged via: ..." line if is_result: merges = [] prefix_len = len('Merged via: ') for rev_str in result_line[prefix_len:].split(','): merges.append(int(rev_str.strip()[1:])) this_item['merges'] = merges # Eat blank line log_lines.pop(0) # Parse output of "Reverse merged via: ..." line if is_result_reverse: reverse_merges = [] prefix_len = len('Reverse merged via: ') for rev_str in result_line[prefix_len:].split(','): reverse_merges.append(int(rev_str.strip()[1:])) this_item['reverse_merges'] = reverse_merges # Eat blank line log_lines.pop(0) # Accumulate the log message msg = '' for line in log_lines[0:lines]: msg += line del log_lines[0:lines] # Maybe accumulate a diff. # If there is a diff, there is a blank line before and after it. if with_diffs and len(log_lines) >= 2 and log_lines[0] == '\n': log_lines.pop(0) diff_lines = [] while len(log_lines) and log_lines[0] != msg_separator: diff_lines.append(log_lines.pop(0)) if diff_lines[-1] == '\n': diff_lines.pop() else: raise SVNLogParseError("no blank line after diff in log") this_item['diff_lines'] = diff_lines elif this_line == msg_separator: if this_item: this_item['msg'] = msg chain.append(this_item) else: # if didn't see separator now, then something's wrong print(this_line) raise SVNLogParseError("trailing garbage after log message") return chain class SVNUnexpectedLogs(svntest.Failure): "Exception raised if a set of log messages doesn't meet expectations." def __init__(self, msg, chain, field_selector = 'revision'): """Stores the log chain for later use. FIELD_SELECTOR indicates which individual field to display when turning the exception into text.""" svntest.Failure.__init__(self, msg) self.chain = chain self.field_selector = field_selector def __str__(self): msg = svntest.Failure.__str__(self) if self.chain: chain_data = list(self.chain) for i in range(0, len(self.chain)): chain_data[i] = self.chain[i][self.field_selector] msg = msg + ': Actual %s list was %s' % (self.field_selector, chain_data) return msg def check_log_chain(chain, revlist, path_counts=[]): """Verify that log chain CHAIN contains the right log messages for revisions START to END (see documentation for parse_log_output() for more about log chains). Do nothing if the log chain's messages run from revision START to END and each log message contains a line of the form 'Log message for revision N' where N is the revision number of that commit. Verify that author and date are present and look sane, but don't check them too carefully. Also verify that even numbered commit messages have three lines. If the length of PATH_COUNTS is greater than zero, make sure that each log has that number of paths. Raise an error if anything looks wrong. """ nbr_expected = len(revlist) if len(chain) != nbr_expected: raise SVNUnexpectedLogs('Number of elements in log chain and revision ' + 'list %s not equal' % revlist, chain) if path_counts and len(path_counts) != nbr_expected: raise SVNUnexpectedLogs('Number of elements in log chain and path ' + 'counts %s not equal' % path_counts, chain) missing_revs = [] for i in range(0, nbr_expected): expect_rev = revlist[i] log_item = chain[i] saw_rev = log_item['revision'] date = log_item['date'] author = log_item['author'] msg = log_item['msg'] # The most important check is that the revision is right: if expect_rev != saw_rev: missing_revs.append(expect_rev) continue # Check that date looks at least vaguely right: date_re = re.compile('[0-9]+') if not date_re.search(date): raise SVNUnexpectedLogs('Malformed date', chain, 'date') # Authors are a little harder, since they might not exist over ra-dav. # Well, it's not much of a check, but we'll do what we can. author_re = re.compile('[a-zA-Z]+') if (not (author_re.search(author) or author == '' or author == '(no author)')): raise SVNUnexpectedLogs('Malformed author', chain, 'author') # Verify the expectation that even-numbered revisions in the Greek # tree tweaked by the log tests have 3-line log messages. if (saw_rev % 2 == 0 and log_item['lines'] != 3): raise SVNUnexpectedLogs('Malformed log line counts', chain, 'lines') # Check that the log message looks right: pattern = 'Log message for revision ' + repr(saw_rev) msg_re = re.compile(pattern) if not msg_re.search(msg): raise SVNUnexpectedLogs("Malformed log message, expected '%s'" % msg, chain) # If path_counts, check the number of changed paths if path_counts: if (not 'paths' in log_item) or (not log_item['paths']): raise SVNUnexpectedLogs("No changed path information", chain) if path_counts[i] != len(log_item['paths']): raise SVNUnexpectedLogs("Changed paths counts not equal for " + "revision %d" % (i + 1), chain) nbr_missing_revs = len(missing_revs) if nbr_missing_revs > 0: raise SVNUnexpectedLogs('Unable to find expected revision(s) %s' % missing_revs, chain) def parse_diff(output): """Return a set containing the various diff bits, broken up by file.""" diff_set = [] current_diff = [] for line in output: if line.startswith('Index: ') and current_diff: diff_set.append(current_diff) current_diff = [] current_diff.append(line) diff_set.append(current_diff) return diff_set def setify(diff_list): """Take a list of lists and make it a set of tuples.""" s = set() for diff in diff_list: s.add(tuple(diff)) return s def compare_diff_output(expected_diffs, output): """Compare the diffs in EXPECTED_DIFFS (which is a Python set) with the text in OUTPUT, remembering that there is no canonical ordering for diffs.""" diffs = parse_diff(output) diffs = setify(diffs) expected_diffs = setify(expected_diffs) if diffs.issubset(expected_diffs) and diffs.issuperset(expected_diffs): return raise svntest.Failure("Diffs not equal") ###################################################################### # Tests # #---------------------------------------------------------------------- def plain_log(sbox): "'svn log', no args, top of wc" guarantee_repos_and_wc(sbox) os.chdir(sbox.wc_dir) exit_code, output, err = svntest.actions.run_and_verify_svn(None, None, [], 'log') log_chain = parse_log_output(output) check_log_chain(log_chain, list(range(max_revision, 1 - 1, -1))) #---------------------------------------------------------------------- def log_with_empty_repos(sbox): "'svn log' on an empty repository" # Create virgin repos svntest.main.safe_rmtree(sbox.repo_dir, 1) svntest.main.create_repos(sbox.repo_dir) svntest.actions.run_and_verify_svn(None, None, [], 'log', sbox.repo_url) #---------------------------------------------------------------------- def log_where_nothing_changed(sbox): "'svn log -rN some_dir_unchanged_in_N'" sbox.build() # Fix bug whereby running 'svn log -rN SOMEPATH' would result in an # xml protocol error if there were no changes in revision N # underneath SOMEPATH. This problem was introduced in revision # 3811, which didn't cover the case where svn_repos_get_logs might # invoke log_receiver zero times. Since the receiver never ran, the # lrb->needs_header flag never got cleared. Control would proceed # without error to the end of dav_svn__log_report(), which would # send a closing tag even though no opening tag had ever been sent. rho_path = os.path.join(sbox.wc_dir, 'A', 'D', 'G', 'rho') svntest.main.file_append(rho_path, "some new material in rho") svntest.actions.run_and_verify_svn(None, None, [], 'ci', '-m', 'log msg', rho_path) # Now run 'svn log -r2' on a directory unaffected by revision 2. H_path = os.path.join(sbox.wc_dir, 'A', 'D', 'H') svntest.actions.run_and_verify_svn(None, None, [], 'log', '-r', '2', H_path) #---------------------------------------------------------------------- def log_to_revision_zero(sbox): "'svn log -v -r 1:0 wc_root'" sbox.build(read_only = True) # This used to segfault the server. svntest.actions.run_and_verify_svn(None, None, [], 'log', '-v', '-r', '1:0', sbox.wc_dir) #---------------------------------------------------------------------- def log_with_path_args(sbox): "'svn log', with args, top of wc" guarantee_repos_and_wc(sbox) os.chdir(sbox.wc_dir) exit_code, output, err = svntest.actions.run_and_verify_svn( None, None, [], 'log', sbox.repo_url, 'A/D/G', 'A/D/H') log_chain = parse_log_output(output) check_log_chain(log_chain, [8, 6, 5, 3, 1]) #---------------------------------------------------------------------- def dynamic_revision(sbox): "'svn log -r COMMITTED' of dynamic/local WC rev" guarantee_repos_and_wc(sbox) os.chdir(sbox.wc_dir) revprops = [{'svn:author': 'jrandom', 'svn:date': '', 'svn:log': 'Log message for revision 9'}] for rev in ('HEAD', 'BASE', 'COMMITTED'): svntest.actions.run_and_verify_log_xml(expected_revprops=revprops, args=['-r', rev]) revprops[0]['svn:log'] = ('Log message for revision 8\n' ' but with multiple lines\n' ' to test the code') svntest.actions.run_and_verify_log_xml(expected_revprops=revprops, args=['-r', 'PREV']) #---------------------------------------------------------------------- def log_wc_with_peg_revision(sbox): "'svn log wc_target@N'" guarantee_repos_and_wc(sbox) my_path = os.path.join(sbox.wc_dir, "A", "B", "E", "beta") + "@8" exit_code, output, err = svntest.actions.run_and_verify_svn(None, None, [], 'log', my_path) check_log_chain(parse_log_output(output), [1]) #---------------------------------------------------------------------- def url_missing_in_head(sbox): "'svn log target@N' when target removed from HEAD" guarantee_repos_and_wc(sbox) my_url = sbox.repo_url + "/A/B/E/alpha" + "@8" exit_code, output, err = svntest.actions.run_and_verify_svn(None, None, [], 'log', my_url) check_log_chain(parse_log_output(output), [3, 1]) #---------------------------------------------------------------------- def log_through_copyfrom_history(sbox): "'svn log TGT' with copyfrom history" sbox.build() wc_dir = sbox.wc_dir msg_file=os.path.join(sbox.repo_dir, 'log-msg') msg_file=os.path.abspath(msg_file) mu_path = os.path.join(wc_dir, 'A', 'mu') mu2_path = os.path.join(wc_dir, 'A', 'mu2') mu_URL = sbox.repo_url + '/A/mu' mu2_URL = sbox.repo_url + '/A/mu2' msg2=""" Log message for revision 2 but with multiple lines to test the code""" msg4=""" Log message for revision 4 but with multiple lines to test the code""" msg6=""" Log message for revision 6 but with multiple lines to test the code""" svntest.main.file_write(msg_file, msg2) svntest.main.file_append(mu_path, "2") svntest.actions.run_and_verify_svn(None, None, [], 'ci', wc_dir, '-F', msg_file) svntest.main.file_append(mu2_path, "this is mu2") svntest.actions.run_and_verify_svn(None, None, [], 'add', mu2_path) svntest.actions.run_and_verify_svn(None, None, [], 'ci', wc_dir, '-m', "Log message for revision 3") svntest.actions.run_and_verify_svn(None, None, [], 'rm', mu2_path) svntest.main.file_write(msg_file, msg4) svntest.actions.run_and_verify_svn(None, None, [], 'ci', wc_dir, '-F', msg_file) svntest.main.file_append(mu_path, "5") svntest.actions.run_and_verify_svn(None, None, [], 'ci', wc_dir, '-m', "Log message for revision 5") svntest.main.file_write(msg_file, msg6) svntest.actions.run_and_verify_svn(None, None, [], 'cp', '-r', '5', mu_URL, mu2_URL, '-F', msg_file) svntest.actions.run_and_verify_svn(None, None, [], 'up', wc_dir) # The full log for mu2 is relatively unsurprising exit_code, output, err = svntest.actions.run_and_verify_svn(None, None, [], 'log', mu2_path) log_chain = parse_log_output(output) check_log_chain(log_chain, [6, 5, 2, 1]) exit_code, output, err = svntest.actions.run_and_verify_svn(None, None, [], 'log', mu2_URL) log_chain = parse_log_output(output) check_log_chain(log_chain, [6, 5, 2, 1]) # First "oddity", the full log for mu2 doesn't include r3, but the -r3 # log works! peg_mu2_path = mu2_path + "@3" exit_code, output, err = svntest.actions.run_and_verify_svn(None, None, [], 'log', '-r', '3', peg_mu2_path) log_chain = parse_log_output(output) check_log_chain(log_chain, [3]) peg_mu2_URL = mu2_URL + "@3" exit_code, output, err = svntest.actions.run_and_verify_svn(None, None, [], 'log', '-r', '3', peg_mu2_URL) log_chain = parse_log_output(output) check_log_chain(log_chain, [3]) exit_code, output, err = svntest.actions.run_and_verify_svn(None, None, [], 'log', '-r', '2', mu2_path) log_chain = parse_log_output(output) check_log_chain(log_chain, [2]) exit_code, output, err = svntest.actions.run_and_verify_svn(None, None, [], 'log', '-r', '2', mu2_URL) log_chain = parse_log_output(output) check_log_chain(log_chain, [2]) #---------------------------------------------------------------------- def escape_control_chars(sbox): "mod_dav_svn must escape invalid XML control chars" dump_str = """SVN-fs-dump-format-version: 2 UUID: ffcae364-69ee-0310-a980-ca5f10462af2 Revision-number: 0 Prop-content-length: 56 Content-length: 56 K 8 svn:date V 27 2005-01-24T10:09:21.759592Z PROPS-END Revision-number: 1 Prop-content-length: 128 Content-length: 128 K 7 svn:log V 100 This msg contains a Ctrl-T (\x14) and a Ctrl-I (\t). The former might be escaped, but the latter never. K 10 svn:author V 7 jrandom K 8 svn:date V 27 2005-01-24T10:09:22.012524Z PROPS-END """ # load dumpfile with control character into repos to get # a log with control char content svntest.actions.load_repo(sbox, dump_str=dump_str) URL = sbox.repo_url # run log exit_code, output, errput = svntest.actions.run_and_verify_svn( None, None, [], 'log', URL) # Verify the output contains either the expected fuzzy escape # sequence, or the literal control char. match_unescaped_ctrl_re = "This msg contains a Ctrl-T \(.\) " \ "and a Ctrl-I \(\t\)\." match_escaped_ctrl_re = "^This msg contains a Ctrl-T \(\?\\\\020\) " \ "and a Ctrl-I \(\t\)\." matched = None for line in output: if re.match(match_unescaped_ctrl_re, line) \ or re.match(match_escaped_ctrl_re, line): matched = 1 if not matched: raise svntest.Failure("log message not transmitted properly:" + str(output) + "\n" + "error: " + str(errput)) #---------------------------------------------------------------------- def log_xml_empty_date(sbox): "svn log --xml must not print empty date elements" sbox.build() # Create the revprop-change hook for this test svntest.actions.enable_revprop_changes(sbox.repo_dir) date_re = re.compile('