업무상 소스의 버전관리 소프트웨어로 CVS를 사용합니다.
( 남들은 SVN 이나 git 를 사용하던데, 장비가 구형 sun 장비다 보니.. )
개발 과정에서 commit을 날리다보면, 내가 무슨 파일을 수정했는지 잊을 때가 있습니다.
개발 건별 패치리스트를 작성할 수 있도록 웹페이지를 하나 만들기로 했습니다.
[ 설계 ]
1. cvs log 명령을 실행하는 shell script를 작성한다.
2. Shell Script 를 실행하여 로그를 읽어 전달하는 jsp 파일을 작성한다.
3. html에서 jsp를 호출 하고, cvs log를 파싱하여 웹 페이지에 보여준다.
[ 개발 ]
1. shell script - ( cvs_log.sh )
#!/usr/bin/ksh cd $1 cvs log -N -S -d ">=$2" 2>& 1 | grep -v "^cvs log" | grep -v "^?"
파라미터로 이동할 cvs 디렉토리와 검색할 날짜를 받습니다.
cvs log -N -S -d ">=2013-06-07"
-. 위 명령은 2013/06/07이후 수정된 파일에 대한 log를 출력해 줍니다.
2>& 1
-. 위 내용은 STDERR을 STDOUT으로 리다이렉션 합니다.
grep -v "^cvs log" | grep -v "^?"
-. 위 명령은 불필요한 로그를 제거 합니다.
2. shell을 실행할 jsp - ( cvsLog.jsp )
<%@ page trimDirectiveWhitespaces="true" %> <%@ page language="java" contentType="text/html;charset=euc-kr" %> <%@ page import="java.net.*" %> <%@ page import="java.io.*" %> <% // no cache response.setDateHeader("Expires", 0); response.setHeader("Pragma", "no-cache"); if (request.getProtocol().equals("HTTP/1.1")) { response.setHeader("Cache-Control", "no-cache"); } // make String projectPath = request.getParameter("projectpath"); String date = request.getParameter("date"); String[] cmd = {"/home1/feel/cms/dev/cvs_log.sh", projectPath, date }; //Run macro on target ProcessBuilder pb = new ProcessBuilder(cmd); pb.directory(new File("/home1/cds")); pb.redirectErrorStream(true); Process process = pb.start(); //Read output StringBuilder sb = new StringBuilder(); BufferedReader br = new BufferedReader( new InputStreamReader( process.getInputStream())); String line = null, previous = null; while ((line = br.readLine()) != null) { if (!line.equals(previous)) { previous = line; sb.append(line).append('\n'); } } //Check result if (process.waitFor() == 0) { out.print(sb.toString()); } %>
jsp 소스는 간단 합니다.
ProcessBuilder, Process 클래스를 이용하여 실행 하고, StreamReader 클래스로 출력된 로그를 입력받습니다.
3. html 파일 - ( patchList.html )
cvs log는 아래와 형식으로 남습니다.
RCS file: /project/MyTest/.cproject,v
Working file: .cproject
head: 1.3
branch:
locks: strict
access list:
keyword substitution: kv
total revisions: 3; selected revisions: 1
description:
----------------------------
revision 1.3
date: 2013/05/28 06:59:43; author: newtype; state: Exp; lines: +260 -182
duid: D0000145
title: 테스트 타이틀
comment: 테스트 commit 상세 내용3
=============================================================================
RCS file: /project/MyTest/.cvsignore,v
Working file: .cvsignore
head: 1.1
branch:
locks: strict
access list:
keyword substitution: kv
total revisions: 1; selected revisions: 1
description:
----------------------------
revision 1.1
date: 2013/05/28 06:59:43; author: newtype; state: Exp;
duid: D0000145
title: 테스트 타이틀
comment: 테스트 commit 상세 내용
=============================================================================
RCS file: /project/MyTest/ifsvr.iml,v
Working file: ifsvr.iml
head: 1.1
branch:
locks: strict
access list:
keyword substitution: b
total revisions: 1; selected revisions: 1
description:
----------------------------
revision 1.1
date: 2013/05/28 06:59:43; author: newtype; state: Exp;
duid: D0000145
title: 테스트 타이틀
comment: 테스트 commit 상세 내용
=============================================================================
RCS file: /project/MyTest/src/CommLib.c,v
Working file: src/CommLib.c
head: 1.2
branch:
locks: strict
access list:
keyword substitution: kv
total revisions: 3; selected revisions: 1
description:
----------------------------
revision 1.2
date: 2013/05/01 00:34:09; author: newtype; state: Exp; lines: +8 -0
도메인을 찾지 못하는 경우 죽어버리는 문제 수정
이런 경우가...;;
=============================================================================
RCS file: /project/MyTest/src/DBLib.pc,v
Working file: src/DBLib.pc
head: 1.203
branch:
locks: strict
access list:
keyword substitution: kv
total revisions: 206; selected revisions: 1
description:
----------------------------
revision 1.203
date: 2013/05/28 06:31:14; author: newtype; state: Exp; lines: +9 -2
duid: D0000145
title: 테스트 타이틀 입니다.
comment: 수정 건에 대한 최초 commit
=============================================================================
RCS file: /project/MyTest/src/HttpGateway.c,v
Working file: src/HttpGateway.c
head: 1.9
branch:
locks: strict
access list:
keyword substitution: kv
total revisions: 9; selected revisions: 1
description:
----------------------------
revision 1.9
date: 2013/05/28 06:31:14; author: newtype; state: Exp; lines: +9 -0
duid: D0000145
title: 테스트 타이틀 입니다.
comment: 수정 건에 대한 최초 commit
=============================================================================
RCS file: /project/MyTest/src/IfSvrLib.c,v
Working file: src/IfSvrLib.c
head: 1.108
branch:
locks: strict
access list:
keyword substitution: kv
total revisions: 109; selected revisions: 2
description:
----------------------------
revision 1.108
date: 2013/05/28 06:59:43; author: newtype; state: Exp; lines: +6 -9
duid: D0000145
title: 테스트 타이틀 입니다.
comment: 이런이런 내용을
블라블라 수정한다.
----------------------------
revision 1.107
date: 2013/05/28 06:31:14; author: newtype; state: Exp; lines: +6 -4
duid: D0000145
title: 테스트 타이틀 입니다.
comment: 수정 건에 대한 최초 commit
=============================================================================
내용을 보면 특정 패턴이 있는 것을 알 수 있습니다.
-. 로그를 파싱하여, json 객체에 담는 javascript는 아래와 같습니다.
function parsingCvsLog(data) { var objs= []; // parsing : ============== var files = data.split(/[\=]{5,}/); for (var i in files) { var obj={historys:[]}; // parsing : Working file: (.....) if ( fn = files[i].match(/Working file\: (.*)\n/) ) { obj.filename = fn[1]; } // parsing : --------------- var historys = files[i].split(/[\-]{5,}/); for(var j in historys ) { var item = {}; var his = []; // parsing : revision (.....) ndate: (.....) author: (.....) if ( his = historys[j] .match(/revision ([.\d]+)\s*\ndate: (.*); author: (\w+).*\n([^$]*)/) ) { item.revision = his[1]; item.date = his[2]; item.author = his[3]; item.comment = his[4]; obj.historys.push(item); } } if ( obj.historys.length !== 0 ) objs.push(obj); } return objs; }
String 검색은 정규식을 사용했습니다.
-. cvs log를 위 함수에 파라미터에 넣고 실행하면, json 객체를 return 합니다.
chrome의 javascript console에서 확인 하면 아래와 같습니다.
-. 완성된 cvs 파서 테스트용 html을 받으려면, 아래 파일을 다른이름으로 저장 하세요.
[ 추가 작업 ]
이렇게 하면 패치 리스트 작성에 문제 있습니다.
실무에서는 개발건이 많기 때문에, 날짜로만 검색하게 되면 불필요한 파일들이 포함 됩니다.
특정 개발건만 구분 하기 위해, 소스를 commit 할때, 아래와 같은 포멧으로 comment를 작성합니다.
duid: D0000145
title: 테스트 타이틀 입니다.
comment: 이런이런 내용을
블라블라 수정한다.
개발 건 마다 uniqe한 ID를 발급하여 할당 하고, 할당한 id를 검색 조건에 포함 하면, 원하는 개발 건으로 수정된 파일 리스트를 얻을 수 있습니다.
리스트를 멋있게 보여주기 위해 Jquery EasyUI를 사용 했습니다.
이것 저것 끼워 넣느라 소스가 좀 길어 졌지만, 깔끔하게 결과가 잘 나오네요.
최종 소스는 아래와 같습니다.
<!DOCTYPE"> <html> <head> <title>Patch List</title> <meta http-equiv="Content-Type" content="text/html; charset=euc-kr"> <META HTTP-EQUIVE="CONTENT-TYPE" CONTENT="TEXT/HTML; CHARSET=KSC5601"> <link rel="stylesheet" type="text/css" href="/jquery-easyui/themes/gray/easyui.css"> <link rel="stylesheet" type="text/css" href="/jquery-easyui/themes/icon.css"> <script src="/jquery/jquery-1.7.1.min.js"></script> <script src="/jquery-easyui/jquery.easyui.min.js"></script> <script src="/jquery-easyui/datagrid-detailview.js"></script> <script language='Javascript'> var PROJECTS = [ // const value { "id" : "MYPROJECT1", "title" : "MyProject1", "projectPath" : "/home/newtype/prj1/", "chagePath" : [ [ "WebRoot/user/", "/home/newtype/prj1/src/user/" ], [ "WebRoot/admin/", "/home/newtype/prj1/src/admin/" ], [ "WebRoot/db/", "/home/newtype/prj1/src/db/" ], [ ".java", ".class" ] ] }, { "id" : "MYPROJECT2", "title" : "MyProject2", "projectPath" : "/home/newtype/prj2/" }, { "id" : "MYPROJECT3", "title" : "MyProject3", "projectPath" : "/home/newtype/test/prj3/" } ]; var index =0; function chagePath(filePath, index) { for(i in PROJECTS[index].chagePath) { filePath = filePath.replace( new RegExp(PROJECTS[index].chagePath[i][0], 'g'), PROJECTS[index].chagePath[i][1] ); } return filePath; } function dateFormatter(date){ var y = date.getFullYear(); var m = date.getMonth()+1; var d = date.getDate(); return y+'-'+(m<10?('0'+m):m)+'-'+(d<10?('0'+d):d); } function dateParser(s){ if (!s) return new Date(); var y = parseInt(s.substr(0,4),10); var m = parseInt(s.substr(5,2),10); var d = parseInt(s.substr(8,2),10); if (!isNaN(y) && !isNaN(m) && !isNaN(d)){ return new Date(y,m-1,d); } else { return new Date(); } } function getToday(){ var d = new Date(); return dateFormatter(d); } function initCombobox() { for(i=0; i<PROJECTS.length; i++) { $("#projectSelectData") .append("<input type='radio' name='projectCurrentIndex' value='"+i+"'><span>" + PROJECTS[i].title +"</span><br/>"); } $("#projectSelect").combo({ required: true, editable:false }) $('#projectSelectData').appendTo($('#projectSelect').combo('panel')); $('#projectSelectData input').click(function(){ var v = $(this).val(); var s = $(this).next('span').text(); $('#projectSelect') .combo('setValue', v) .combo('setText', s) .combo('hidePanel'); index = v; }); } function parsingCvsLog(data) { var objs= []; var files = data.split(/[\=]{5,}/); for (var i in files) { var obj={historys:[]}; if ( fn = files[i].match(/Working file\: (.*)\n/) ) { obj.filename = chagePath(fn[1], index); } var historys = files[i].split(/[\-]{5,}/); for(var j in historys ) { var item = {}; var his = []; var comment; if ( his = historys[j] .match(/revision ([.\d]+)\s*\ndate: (.*); author: (\w+).*\n([^$]*)/) ) { item.revision = his[1]; item.date = his[2]; item.author = his[3]; comment = his[4]; if ( his = comment .match(/duid:\s*(\w+)\s*title:\s*(.+)\ncomment:\s*([^$]*)/) ) { item.duid = his[1]; item.title = his[2]; item.comment = his[3]; } else { item.comment = comment; } obj.historys.push(item); } } if ( obj.historys.length !== 0 ) { obj.historycount = obj.historys.length; objs.push(obj); } } return objs; } $(document).ready(function(){ initCombobox(); /* default index is '0'. */ $('#projectSelectData input:eq(' + index + ')').click(); $("#data").hide(); $("#inputDate").datebox('setValue', getToday()); $("#btnDoIt").click(function(){ $("#data").hide(); $("#btnDoIt").attr("value", "Loading").attr("disabled", true); $.ajax({ url: "./cvsLog.jsp?projectpath="+PROJECTS[index].projectPath +"&date="+$("#inputDate").datebox('getValue') +"&duid="+$('#duid').val(), type: 'GET', dataType: 'Text', timeout: 30000, error: function(xhr, textStatus, errorThrown){ $("#data").hide(); $("#btnDoIt").attr("value", "패치 리스트 조회") .attr("disabled", false); $("#nodataMsg").show() .fadeOut(200).fadeIn(200).fadeOut(200).fadeIn(200); }, success: function(data, status, xhr){ if ( data.substr(0,5) == 'ERROR' ) { $("#data").hide(); $("#nodataMsg").show() .fadeOut(200).fadeIn(200) .fadeOut(200).fadeIn(200); $("#btnDoIt").attr("value", "패치 리스트 조회") .attr("disabled", false); return; } var cvsData = parsingCvsLog(data); // filter if ( $("#duid").val() != "" ) { for(var i=cvsData.length; i--;) { for(var j=cvsData[i].historys.length; j--;) { if ( cvsData[i].historys[j].duid != $("#duid").val() ) { cvsData[i].historys.splice(j,1); } } if (cvsData[i].historys.length == 0) { cvsData.splice(i,1); } } } if (cvsData.length <= 0) { $("#data").hide(); $("#nodataMsg").show() .fadeOut(200).fadeIn(200).fadeOut(200).fadeIn(200); } else { $("#nodataMsg").hide(data); $("#data").show(); $("#fileCount").text(cvsData.length); $("#dataGrid").datagrid('loadData', cvsData); } $("#btnDoIt") .attr("value", "패치 리스트 조회").attr("disabled", false); } }); }); $(function(){ $('#dataGrid').datagrid({ view: detailview, detailFormatter:function(index,row){ return '<div style="padding:2px"><table id="ddv-' + index + '"></table></div>'; }, onExpandRow: function(index,row){ $('#ddv-'+index).datagrid({ data: row.historys, fitColumns:false, singleSelect:true, loadMsg:'loding...', height:'auto', columns:[[ {field:'author',title:'개발자' ,width:100,align:'center'}, {field:'date',title:'Date',width:150,align:'left'}, {field:'duid',title:'DUID',width:150,align:'center'}, {field:'title',title:'Title',width:500,align:'center'} ]], onResize:function(){ $('#dataGrid').datagrid('fixDetailRowHeight',index); }, onLoadSuccess:function(){ setTimeout(function(){ $('#dataGrid') .datagrid('fixDetailRowHeight',index); },0); }, onDblClickRow:function(rowIndex, rowData){ $.messager.show({ title:'Comment', msg: '<span class=ctitle>revision: </span> ' + rowData.revision + '<br/>' + '<span class=ctitle>comment: </span> ' + rowData.comment.replace(/\n/g, "<br/>"), width: 300, height: 300, style:{ right:'', bottom:'' } }); } }); $('#dataGrid').datagrid('fixDetailRowHeight',index); } }); }); }); </script> <style type="text/css"> * { font-size:12px; font-family:돋움 } .ctitle { color: blue; font-weight: bold; } </style> </head> <body> <table border="0" width="150"> <tr> <td width="330" align="center"> <font size="2" color="#2D7776"> <b>Patch List</b> </font> </td> </tr> </table> <br/> <table border="0" width="90%" height="30"> <tr> <td width="330" align="left"> <select id="projectSelect" style="width:300px"></select> <div id="projectSelectData"> <div style="color:#99BBE8;background:#fafafa;padding:5px;"> Select a Project </div> </div> </td> </tr> </table> <table width="1000" border="1" cellpadding="0" cellspacing="0" bordercolor="#efefef" style="border-collapse: collapse"> <tr align="right" bgcolor="#efefef"> <td bgcolor="#efefef" align="center" width="150"> <font color="#000000" size="2">조회 일자</font> </td> <td bgcolor="#ffffff" align="left" width="200"> <input id="inputDate" class="easyui-datebox" data-options="formatter:dateFormatter,parser:dateParser"/> </td> <td bgcolor="#ffffff" align="left" > DUID: <input type="text" id="duid"/> </td> <td bgcolor="#ffffff" align="left" width="200"> <input type="button" id="btnDoIt" value="패치 리스트 조회"/> </td> </tr> </table> <br/> <br/> <table border="0" cellpadding="0" cellspacing="0" width="1000" id="data"> <tr> <td height="25" align="left" valign="middle" bgcolor="white"> <div>File Count: <span id="fileCount"></span> 건</div> </td> </tr> <tr> <td height="25" align="left" valign="middle" bgcolor="white"> <table id="dataGrid" class="easyui-datagrid" title="패치 리스트" data-options="singleSelect:true"> <thead> <tr> <th data-options="field:'filename',width:850,align:'left'"> 파일명 </th> <th data-options="field:'historycount',width:120,align:'left'"> Historys Count </th> </tr> </thead> </table> </td> <tr> </table> <table border="0" cellpadding="0" cellspacing="0" width="1000" id="nodataMsg"> <tr> <td height="25" align="center" colspan="5" valign="middle" bgcolor="white"> <p><font size="2" face="돋움"> <br/> <div id=msg>::: 데이터가 미존재 합니다. :::</div><br/> </font> </p> </td> <tr> </table> </body> </html>
'Dev > Web' 카테고리의 다른 글
카카오톡 그룹 채팅방 통계 (2) | 2013.12.17 |
---|---|
Web Notepad (0) | 2013.12.05 |
IOS 토글 버튼 (0) | 2012.12.17 |
Apache 특정 IP만 접근 가능하게 허용. (0) | 2012.07.26 |
Javascript로 폭포수 바이러스 효과 구현.. (0) | 2011.08.04 |